├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── tests ├── bkcrack │ ├── data │ │ ├── empty.zip │ │ ├── plain.zip │ │ ├── zip64.zip │ │ ├── aes256.zip │ │ ├── zipcrypto.zip │ │ ├── zip64-zipcrypto.zip │ │ └── make_test_data.sh │ ├── CMakeLists.txt │ ├── Progress.test.cpp │ ├── log.test.cpp │ ├── types.test.cpp │ ├── MultTab.test.cpp │ ├── KeystreamTab.test.cpp │ ├── file.test.cpp │ ├── Crc32Tab.test.cpp │ ├── Keys.test.cpp │ ├── Zreduction.test.cpp │ ├── Attack.test.cpp │ ├── Data.test.cpp │ ├── password.test.cpp │ └── Zip.test.cpp ├── cli │ ├── CMakeLists.txt │ ├── SigintHandler.test.cpp │ ├── ConsoleProgress.test.cpp │ └── Arguments.test.cpp ├── runner │ ├── main.cpp │ ├── TestRunner-pass.test.cpp │ ├── TestRunner-fail.test.cpp │ ├── CMakeLists.txt │ ├── TestRunner.cpp │ └── TestRunner.hpp ├── verify_hash.cmake ├── decrypt.cmake ├── change-password.cmake ├── change-keys.cmake ├── decipher.cmake └── CMakeLists.txt ├── example ├── secrets.zip └── tutorial.md ├── src ├── bkcrack │ ├── Progress.cpp │ ├── types.cpp │ ├── KeystreamTab.cpp │ ├── MultTab.cpp │ ├── Crc32Tab.cpp │ ├── log.cpp │ ├── Keys.cpp │ ├── CMakeLists.txt │ ├── file.cpp │ ├── Zreduction.cpp │ ├── Data.cpp │ └── Attack.cpp └── cli │ ├── utf-8.manifest │ ├── VirtualTerminalSupport.hpp │ ├── CMakeLists.txt │ ├── SigintHandler.cpp │ ├── SigintHandler.hpp │ ├── ConsoleProgress.hpp │ ├── VirtualTerminalSupport.cpp │ ├── ConsoleProgress.cpp │ └── Arguments.hpp ├── doc ├── index.md ├── limitations.md ├── CMakeLists.txt └── resources.md ├── gcovr.cfg ├── include └── bkcrack │ ├── version.hpp.in │ ├── log.hpp │ ├── file.hpp │ ├── Zreduction.hpp │ ├── Progress.hpp │ ├── Crc32Tab.hpp │ ├── MultTab.hpp │ ├── types.hpp │ ├── KeystreamTab.hpp │ ├── Data.hpp │ ├── Keys.hpp │ ├── password.hpp │ ├── Attack.hpp │ └── Zip.hpp ├── tools ├── inflate.py └── deflate.py ├── license.txt ├── .clang-format ├── CMakeLists.txt └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kimci86 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | install 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /tests/bkcrack/data/empty.zip: -------------------------------------------------------------------------------- 1 | PK -------------------------------------------------------------------------------- /example/secrets.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimci86/bkcrack/HEAD/example/secrets.zip -------------------------------------------------------------------------------- /tests/bkcrack/data/plain.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimci86/bkcrack/HEAD/tests/bkcrack/data/plain.zip -------------------------------------------------------------------------------- /tests/bkcrack/data/zip64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimci86/bkcrack/HEAD/tests/bkcrack/data/zip64.zip -------------------------------------------------------------------------------- /tests/bkcrack/data/aes256.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimci86/bkcrack/HEAD/tests/bkcrack/data/aes256.zip -------------------------------------------------------------------------------- /tests/bkcrack/data/zipcrypto.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimci86/bkcrack/HEAD/tests/bkcrack/data/zipcrypto.zip -------------------------------------------------------------------------------- /tests/bkcrack/data/zip64-zipcrypto.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimci86/bkcrack/HEAD/tests/bkcrack/data/zip64-zipcrypto.zip -------------------------------------------------------------------------------- /src/bkcrack/Progress.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/Progress.hpp" 2 | 3 | Progress::Progress(std::ostream& os) 4 | : m_os{os} 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /tests/cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB TESTS "*.test.cpp") 2 | foreach(test IN ITEMS ${TESTS}) 3 | bkcrack_add_unittest(${test} bkcrack-cli) 4 | endforeach() 5 | -------------------------------------------------------------------------------- /tests/bkcrack/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB TESTS "*.test.cpp") 2 | foreach(test IN ITEMS ${TESTS}) 3 | bkcrack_add_unittest(${test} bkcrack-core) 4 | endforeach() 5 | -------------------------------------------------------------------------------- /tests/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | auto main() -> int 4 | { 5 | const auto success = TestRunner::runAllTests(); 6 | return success ? 0 : 1; 7 | } 8 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | Documentation {#index} 2 | ============= 3 | 4 | Welcome to the documentation. 5 | 6 | \if not_doxygen 7 | 8 | **Use doxygen to generate the HTML documentation.** 9 | 10 | \endif 11 | -------------------------------------------------------------------------------- /gcovr.cfg: -------------------------------------------------------------------------------- 1 | gcov-ignore-parse-errors = negative_hits.warn 2 | exclude-throw-branches = yes 3 | html-title = bkcrack code coverage report 4 | html-theme = github.green 5 | print-summary = yes 6 | exclude = tests/ 7 | -------------------------------------------------------------------------------- /src/bkcrack/types.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/types.hpp" 2 | 3 | BaseError::BaseError(const std::string& type, const std::string& description) 4 | : std::runtime_error{type + ": " + description + "."} 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /include/bkcrack/version.hpp.in: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_VERSION_HPP 2 | #define BKCRACK_VERSION_HPP 3 | 4 | constexpr auto bkcrackVersion = "@bkcrack_VERSION@"; 5 | constexpr auto bkcrackVersionDate = "@bkcrack_VERSION_DATE@"; 6 | 7 | #endif // BKCRACK_VERSION_HPP 8 | -------------------------------------------------------------------------------- /doc/limitations.md: -------------------------------------------------------------------------------- 1 | Known limitations {#limitations} 2 | ================= 3 | 4 | \brief The list of known limitations and unsupported features. 5 | 6 | \if not_doxygen 7 | 8 | **The list is generated by doxygen.** 9 | **The special command alias \limitation is used in doxygen input files to populate the list.** 10 | 11 | \endif 12 | -------------------------------------------------------------------------------- /tests/bkcrack/Progress.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | TEST("log") 8 | { 9 | auto os = std::ostringstream{}; 10 | auto progress = Progress{os}; 11 | progress.log([](std::ostream& os) { os << "test" << std::endl; }); 12 | CHECK(os.str() == "test\n"); 13 | } 14 | -------------------------------------------------------------------------------- /tests/runner/TestRunner-pass.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | TEST("simple check") 6 | { 7 | CHECK(1 + 1 == 2); 8 | } 9 | 10 | TEST("expected exception with message") 11 | { 12 | const auto throwing = [] { throw std::runtime_error{"runtime error"}; }; 13 | CHECK_THROWS(std::runtime_error, "error", throwing()); 14 | } 15 | -------------------------------------------------------------------------------- /tests/verify_hash.cmake: -------------------------------------------------------------------------------- 1 | function(verify_hash INPUT_FILE APPROVED_HASH) 2 | file(SHA256 ${INPUT_FILE} RECEIVED_HASH) 3 | if(RECEIVED_HASH STREQUAL ${APPROVED_HASH}) 4 | message(STATUS "${INPUT_FILE} hash verification succeeded") 5 | else() 6 | message(FATAL_ERROR "${INPUT_FILE} hash verification failed:\n approved: ${APPROVED_HASH}\n received: ${RECEIVED_HASH}") 7 | endif() 8 | endfunction() 9 | -------------------------------------------------------------------------------- /tests/cli/SigintHandler.test.cpp: -------------------------------------------------------------------------------- 1 | #include "SigintHandler.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | TEST("set progress state upon SIGINT") 8 | { 9 | auto state = std::atomic{Progress::State::Normal}; 10 | auto handler = SigintHandler{state}; 11 | CHECK(state == Progress::State::Normal); 12 | std::raise(SIGINT); 13 | CHECK(state == Progress::State::Canceled); 14 | } 15 | -------------------------------------------------------------------------------- /tests/decrypt.cmake: -------------------------------------------------------------------------------- 1 | include(${PROJECT_SOURCE_DIR}/tests/verify_hash.cmake) 2 | 3 | execute_process( 4 | COMMAND ${BKCRACK_COMMAND} 5 | -C ${PROJECT_SOURCE_DIR}/example/secrets.zip 6 | -k c4490e28 b414a23d 91404b31 7 | -D decrypt.zip 8 | COMMAND_ERROR_IS_FATAL ANY) 9 | verify_hash(decrypt.zip 7365b22e535e545fc60952e82acea961dce512d939dd01545e0a20f7fe82bb8e) 10 | file(REMOVE decrypt.zip) 11 | -------------------------------------------------------------------------------- /tools/inflate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import zlib 5 | 6 | def inflate(data): 7 | """Returns uncompressed data.""" 8 | return zlib.decompress(data, -zlib.MAX_WBITS) 9 | 10 | def main(): 11 | """Read deflate compressed data from stdin and write uncompressed data to stdout.""" 12 | sys.stdout.buffer.write(inflate(sys.stdin.buffer.read())) 13 | 14 | if __name__ == "__main__": 15 | main() 16 | -------------------------------------------------------------------------------- /tests/change-password.cmake: -------------------------------------------------------------------------------- 1 | include(${PROJECT_SOURCE_DIR}/tests/verify_hash.cmake) 2 | 3 | execute_process( 4 | COMMAND ${BKCRACK_COMMAND} 5 | -C ${PROJECT_SOURCE_DIR}/example/secrets.zip 6 | -k c4490e28 b414a23d 91404b31 7 | -U change-password.zip new-password 8 | COMMAND_ERROR_IS_FATAL ANY) 9 | verify_hash(change-password.zip 1aac8f747b205074ca662b533fa421f160b81d4f1afa99b750997d4038a49ebc) 10 | file(REMOVE change-password.zip) 11 | -------------------------------------------------------------------------------- /tests/change-keys.cmake: -------------------------------------------------------------------------------- 1 | include(${PROJECT_SOURCE_DIR}/tests/verify_hash.cmake) 2 | 3 | execute_process( 4 | COMMAND ${BKCRACK_COMMAND} 5 | -C ${PROJECT_SOURCE_DIR}/example/secrets.zip 6 | -k c4490e28 b414a23d 91404b31 7 | --change-keys change-keys.zip 86484f1d 3fb4c16f ba11de5e 8 | COMMAND_ERROR_IS_FATAL ANY) 9 | verify_hash(change-keys.zip 1aac8f747b205074ca662b533fa421f160b81d4f1afa99b750997d4038a49ebc) 10 | file(REMOVE change-keys.zip) 11 | -------------------------------------------------------------------------------- /tests/decipher.cmake: -------------------------------------------------------------------------------- 1 | include(${PROJECT_SOURCE_DIR}/tests/verify_hash.cmake) 2 | 3 | execute_process( 4 | COMMAND ${BKCRACK_COMMAND} 5 | -C ${PROJECT_SOURCE_DIR}/example/secrets.zip 6 | -c advice.jpg 7 | -k c4490e28 b414a23d 91404b31 8 | -d decipher.advice.deflate 9 | COMMAND_ERROR_IS_FATAL ANY) 10 | verify_hash(decipher.advice.deflate de3b1050d1ce81bcebaa9ea2c2481f7466c47188ca6ae3e9509975e68fd834da) 11 | file(REMOVE decipher.advice.deflate) 12 | -------------------------------------------------------------------------------- /src/cli/utf-8.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UTF-8 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/bkcrack/KeystreamTab.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/KeystreamTab.hpp" 2 | 3 | const KeystreamTab KeystreamTab::instance; 4 | 5 | KeystreamTab::KeystreamTab() 6 | { 7 | for (auto z_2_16 = std::uint32_t{}; z_2_16 < 1 << 16; z_2_16 += 4) 8 | { 9 | const auto k = lsb((z_2_16 | 2) * (z_2_16 | 3) >> 8); 10 | 11 | keystreamtab[z_2_16 >> 2] = k; 12 | keystreaminvfiltertab[k][z_2_16 >> 10].push_back(z_2_16); 13 | keystreaminvexists[k].set(z_2_16 >> 10); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /include/bkcrack/log.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_LOG_HPP 2 | #define BKCRACK_LOG_HPP 3 | 4 | #include 5 | 6 | /// \file log.hpp 7 | /// \brief Output stream manipulators 8 | 9 | /// Insert the current local time into the output stream 10 | auto put_time(std::ostream& os) -> std::ostream&; 11 | 12 | class Keys; // forward declaration 13 | 14 | /// \brief Insert a representation of keys into the stream \a os 15 | /// \relates Keys 16 | auto operator<<(std::ostream& os, const Keys& keys) -> std::ostream&; 17 | 18 | #endif // BKCRACK_LOG_HPP 19 | -------------------------------------------------------------------------------- /tests/bkcrack/log.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | TEST("put_time") 10 | { 11 | auto os = std::ostringstream{}; 12 | os << put_time; 13 | CHECK(std::regex_match(os.str(), std::regex{R"([0-2][0-9]:[0-5][0-9]:[0-5][0-9])"})); 14 | } 15 | 16 | TEST("Keys output operator") 17 | { 18 | auto os = std::ostringstream{}; 19 | os << Keys{0x382bd98d, 0x5ad55f3b, 0x04f8d2f6}; 20 | CHECK(os.str() == "382bd98d 5ad55f3b 04f8d2f6"); 21 | } 22 | -------------------------------------------------------------------------------- /src/bkcrack/MultTab.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/MultTab.hpp" 2 | 3 | const MultTab MultTab::instance; 4 | 5 | MultTab::MultTab() 6 | { 7 | auto prodinv = std::uint32_t{}; // x * mult^-1 8 | for (auto x = 0; x < 256; x++, prodinv += multInv) 9 | { 10 | msbprodfiber2[msb(prodinv)].push_back(x); 11 | msbprodfiber2[(msb(prodinv) + 1) % 256].push_back(x); 12 | 13 | msbprodfiber3[(msb(prodinv) + 255) % 256].push_back(x); 14 | msbprodfiber3[msb(prodinv)].push_back(x); 15 | msbprodfiber3[(msb(prodinv) + 1) % 256].push_back(x); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/cli/VirtualTerminalSupport.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_VIRTUALTERMINALSUPPORT_HPP 2 | #define BKCRACK_VIRTUALTERMINALSUPPORT_HPP 3 | 4 | #include 5 | 6 | /// \brief Class to enable virtual terminal support 7 | /// 8 | /// It is useful only on Windows. It does nothing on other platforms. 9 | class VirtualTerminalSupport 10 | { 11 | public: 12 | /// Enable virtual terminal support 13 | VirtualTerminalSupport(); 14 | 15 | /// Restore console mode as it was before 16 | ~VirtualTerminalSupport(); 17 | 18 | private: 19 | class Impl; // platform-specific implementation 20 | 21 | std::unique_ptr m_impl; 22 | }; 23 | 24 | #endif // BKCRACK_VIRTUALTERMINALSUPPORT_HPP 25 | -------------------------------------------------------------------------------- /src/bkcrack/Crc32Tab.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/Crc32Tab.hpp" 2 | 3 | const Crc32Tab Crc32Tab::instance; 4 | 5 | Crc32Tab::Crc32Tab() 6 | { 7 | // CRC32 polynomial representation 8 | constexpr auto crcPolynom = 0xedb88320; 9 | 10 | for (auto b = 0; b < 256; b++) 11 | { 12 | auto crc = static_cast(b); 13 | // compute CRC32 from the original definition 14 | for (auto i = 0; i < 8; i++) 15 | if (crc & 1) 16 | crc = crc >> 1 ^ crcPolynom; 17 | else 18 | crc = crc >> 1; 19 | 20 | // fill lookup tables 21 | crctab[b] = crc; 22 | crcinvtab[msb(crc)] = crc << 8 ^ b; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/bkcrack/log.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/log.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | auto put_time(std::ostream& os) -> std::ostream& 9 | { 10 | const auto t = std::time(nullptr); 11 | return os << std::put_time(std::localtime(&t), "%T"); 12 | } 13 | 14 | auto operator<<(std::ostream& os, const Keys& keys) -> std::ostream& 15 | { 16 | const auto flagsBefore = os.setf(std::ios::hex, std::ios::basefield); 17 | const auto fillBefore = os.fill('0'); 18 | 19 | os << std::setw(8) << keys.getX() << " " << std::setw(8) << keys.getY() << " " << std::setw(8) << keys.getZ(); 20 | 21 | os.fill(fillBefore); 22 | os.flags(flagsBefore); 23 | 24 | return os; 25 | } 26 | -------------------------------------------------------------------------------- /src/cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # list files 2 | file(GLOB HEADERS "*.hpp") 3 | file(GLOB SOURCES "*.cpp") 4 | list(FILTER SOURCES EXCLUDE REGEX "main\\.cpp") 5 | 6 | # add the library target 7 | add_library(bkcrack-cli) 8 | target_sources(bkcrack-cli 9 | PUBLIC FILE_SET HEADERS 10 | FILES 11 | ${HEADERS} 12 | PRIVATE 13 | ${SOURCES} 14 | ) 15 | target_enable_warnings(bkcrack-cli) 16 | target_link_libraries(bkcrack-cli PUBLIC bkcrack-core) 17 | 18 | # add the executable target 19 | add_executable(bkcrack) 20 | target_sources(bkcrack PRIVATE main.cpp utf-8.manifest) 21 | target_enable_warnings(bkcrack) 22 | target_link_libraries(bkcrack PRIVATE bkcrack-cli) 23 | 24 | # install rules 25 | install(TARGETS bkcrack DESTINATION .) 26 | -------------------------------------------------------------------------------- /src/cli/SigintHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "SigintHandler.hpp" 2 | 3 | #include 4 | 5 | namespace 6 | { 7 | 8 | static_assert(std::atomic::is_always_lock_free, "atomics must be lock-free to be signal-safe"); 9 | 10 | std::atomic* destination = nullptr; 11 | 12 | } // namespace 13 | 14 | void bkcrackSigintHandler(int sig) 15 | { 16 | *destination = Progress::State::Canceled; 17 | std::signal(sig, &bkcrackSigintHandler); 18 | } 19 | 20 | SigintHandler::SigintHandler(std::atomic& destination) 21 | { 22 | ::destination = &destination; 23 | std::signal(SIGINT, &bkcrackSigintHandler); 24 | } 25 | 26 | SigintHandler::~SigintHandler() 27 | { 28 | std::signal(SIGINT, SIG_DFL); 29 | destination = nullptr; 30 | } 31 | -------------------------------------------------------------------------------- /tests/cli/ConsoleProgress.test.cpp: -------------------------------------------------------------------------------- 1 | #include "ConsoleProgress.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | TEST("print progress regularly") 8 | { 9 | auto oss = std::ostringstream{}; 10 | { 11 | auto progress = ConsoleProgress{oss, std::chrono::milliseconds{20}}; 12 | progress.total = 10; 13 | CHECK(oss.str() == ""); 14 | 15 | std::this_thread::sleep_for(std::chrono::milliseconds{500}); 16 | CHECK(oss.str().ends_with("0.0 % (0 / 10)\033[1K\r")); 17 | 18 | progress.done = 9; 19 | std::this_thread::sleep_for(std::chrono::milliseconds{500}); 20 | CHECK(oss.str().ends_with("90.0 % (9 / 10)\033[1K\r")); 21 | 22 | progress.done = 10; 23 | } 24 | CHECK(oss.str().ends_with("100.0 % (10 / 10)\n")); 25 | } 26 | -------------------------------------------------------------------------------- /src/cli/SigintHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_SIGINTHANDLER_HPP 2 | #define BKCRACK_SIGINTHANDLER_HPP 3 | 4 | #include 5 | 6 | /// \brief Utility class to set a progress state to Progress::State::Canceled when SIGINT arrives 7 | /// 8 | /// \note There should exist at most one instance of this class at any time. 9 | class SigintHandler 10 | { 11 | public: 12 | /// Enable the signal handler 13 | explicit SigintHandler(std::atomic& destination); 14 | 15 | /// Disable the signal handler 16 | ~SigintHandler(); 17 | 18 | /// Deleted copy constructor 19 | SigintHandler(const SigintHandler& other) = delete; 20 | 21 | /// Deleted assignment operator 22 | auto operator=(const SigintHandler& other) -> SigintHandler& = delete; 23 | }; 24 | 25 | #endif // BKCRACK_SIGINTHANDLER_HPP 26 | -------------------------------------------------------------------------------- /src/bkcrack/Keys.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/Keys.hpp" 2 | 3 | Keys::Keys(std::uint32_t x, std::uint32_t y, std::uint32_t z) 4 | : x{x} 5 | , y{y} 6 | , z{z} 7 | { 8 | } 9 | 10 | Keys::Keys(const std::string& password) 11 | { 12 | for (const auto p : password) 13 | update(p); 14 | } 15 | 16 | void Keys::update(const std::vector& ciphertext, std::size_t current, std::size_t target) 17 | { 18 | for (auto i = ciphertext.begin() + current; i != ciphertext.begin() + target; ++i) 19 | update(*i ^ getK()); 20 | } 21 | 22 | void Keys::updateBackward(const std::vector& ciphertext, std::size_t current, std::size_t target) 23 | { 24 | for (auto i = std::reverse_iterator{ciphertext.begin() + current}; 25 | i != std::reverse_iterator{ciphertext.begin() + target}; ++i) 26 | updateBackward(*i); 27 | } 28 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # find doxygen 2 | find_package(Doxygen REQUIRED) 3 | 4 | # doxygen configuration 5 | set(DOXYGEN_STRIP_FROM_PATH "${PROJECT_SOURCE_DIR}") 6 | set(DOXYGEN_STRIP_FROM_INC_PATH "${PROJECT_SOURCE_DIR}/include;${PROJECT_SOURCE_DIR}") 7 | set(DOXYGEN_FILE_PATTERNS "*.hpp;*.md") 8 | set(DOXYGEN_QUIET "YES") 9 | set(DOXYGEN_SEARCHENGINE "NO") 10 | set(DOXYGEN_ALIASES [[limitation=\xrefitem limitations \"Known limitations\" \"Known limitations\"]]) 11 | set(DOXYGEN_DISTRIBUTE_GROUP_DOC "YES") 12 | 13 | # add target 14 | doxygen_add_docs(doc ALL 15 | "${PROJECT_SOURCE_DIR}/doc" 16 | "${PROJECT_SOURCE_DIR}/include" 17 | "${PROJECT_SOURCE_DIR}/src" 18 | "${PROJECT_SOURCE_DIR}/example" 19 | COMMENT "Generating HTML documentation with doxygen") 20 | 21 | # install rules 22 | install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/html" DESTINATION doc) 23 | -------------------------------------------------------------------------------- /include/bkcrack/file.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_FILE_HPP 2 | #define BKCRACK_FILE_HPP 3 | 4 | #include 5 | 6 | #include 7 | 8 | /// Exception thrown if a file cannot be opened 9 | class FileError : public BaseError 10 | { 11 | public: 12 | /// Constructor 13 | explicit FileError(const std::string& description); 14 | }; 15 | 16 | /// \brief Open an input file stream 17 | /// \exception FileError if the file cannot be opened 18 | auto openInput(const std::string& filename) -> std::ifstream; 19 | 20 | /// Load at most \a size bytes from an input stream 21 | auto loadStream(std::istream& is, std::size_t size) -> std::vector; 22 | 23 | /// \brief Open an output file stream 24 | /// \exception FileError if the file cannot be opened 25 | auto openOutput(const std::string& filename) -> std::ofstream; 26 | 27 | #endif // BKCRACK_FILE_HPP 28 | -------------------------------------------------------------------------------- /tests/bkcrack/data/make_test_data.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -f *.zip 4 | 5 | echo | zip empty.zip - 6 | zip -d empty.zip - 7 | 8 | echo store\ {A..Z} > store.txt 9 | echo deflate\ {A..Z} > deflate.txt 10 | 11 | zip -X -Z store plain.zip store.txt 12 | zip -X -Z deflate plain.zip deflate.txt 13 | 14 | zip -X -Z store -e -P password zipcrypto.zip store.txt 15 | zip -X -Z deflate -e -P password zipcrypto.zip deflate.txt 16 | 17 | zip -X -Z store --force-zip64 zip64.zip store.txt 18 | zip -X -Z deflate --force-zip64 zip64.zip deflate.txt 19 | 20 | zip -X -Z store -e -P password --force-zip64 zip64-zipcrypto.zip store.txt 21 | zip -X -Z deflate -e -P password --force-zip64 zip64-zipcrypto.zip deflate.txt 22 | 23 | 7z a -mm=store -mem=aes256 -ppassword aes256.zip store.txt 24 | 7z a -mm=deflate -mem=aes256 -ppassword aes256.zip deflate.txt 25 | 26 | rm store.txt deflate.txt 27 | -------------------------------------------------------------------------------- /tests/runner/TestRunner-fail.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | TEST("failed check") 6 | { 7 | CHECK(1 + 1 != 2); 8 | } 9 | 10 | TEST("missing exception") 11 | { 12 | const auto notThrowing = [] {}; 13 | CHECK_THROWS(std::runtime_error, "", notThrowing()); 14 | } 15 | 16 | TEST("mismatching exception type") 17 | { 18 | const auto throwing = [] { throw std::invalid_argument{"invalid argument"}; }; 19 | CHECK_THROWS(std::runtime_error, "", throwing()); 20 | } 21 | 22 | TEST("mismatching exception message") 23 | { 24 | const auto throwing = [] { throw std::invalid_argument{"invalid argument"}; }; 25 | CHECK_THROWS(std::invalid_argument, "error", throwing()); 26 | } 27 | 28 | TEST("throw exception") 29 | { 30 | throw std::runtime_error{"runtime error"}; 31 | } 32 | 33 | TEST("throw something else") 34 | { 35 | throw 42; 36 | } 37 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The zlib/libpng License 2 | 3 | Copyright (c) 2016-2025 Joachim Hotonnier 4 | 5 | This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 8 | 9 | 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 10 | 11 | 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 12 | 13 | 3. This notice may not be removed or altered from any source distribution. 14 | -------------------------------------------------------------------------------- /src/bkcrack/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # configure version header 2 | set(VERSION_HEADER_CONFIGURED "${PROJECT_BINARY_DIR}/include/bkcrack/version.hpp") 3 | configure_file("${PROJECT_SOURCE_DIR}/include/bkcrack/version.hpp.in" "${VERSION_HEADER_CONFIGURED}") 4 | 5 | # list files 6 | file(GLOB HEADERS "${PROJECT_SOURCE_DIR}/include/bkcrack/*.hpp") 7 | file(GLOB SOURCES "*.cpp") 8 | 9 | # add the library target 10 | add_library(bkcrack-core) 11 | target_sources(bkcrack-core 12 | PUBLIC FILE_SET HEADERS 13 | BASE_DIRS ${PROJECT_SOURCE_DIR}/include ${PROJECT_BINARY_DIR}/include 14 | FILES 15 | ${VERSION_HEADER_CONFIGURED} 16 | ${HEADERS} 17 | PRIVATE 18 | ${SOURCES} 19 | ) 20 | target_enable_warnings(bkcrack-core) 21 | 22 | # enable C++17 23 | target_compile_features(bkcrack-core PUBLIC cxx_std_17) 24 | 25 | # let CMake work out the system-specific details to use threads 26 | find_package(Threads REQUIRED) 27 | target_link_libraries(bkcrack-core PRIVATE Threads::Threads) 28 | -------------------------------------------------------------------------------- /tests/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(bkcrack-testrunner) 2 | target_sources(bkcrack-testrunner 3 | PUBLIC FILE_SET HEADERS 4 | FILES 5 | TestRunner.hpp 6 | PRIVATE 7 | TestRunner.cpp 8 | main.cpp 9 | ) 10 | target_enable_warnings(bkcrack-testrunner) 11 | target_compile_features(bkcrack-testrunner PUBLIC cxx_std_20) 12 | 13 | # function for unit test definition and registration 14 | function(bkcrack_add_unittest file) 15 | cmake_path(GET file STEM stem) 16 | string(TOLOWER ${stem} name) 17 | add_executable(unittest-${name} ${file}) 18 | target_enable_warnings(unittest-${name}) 19 | target_link_libraries(unittest-${name} PRIVATE bkcrack-testrunner ${ARGN}) 20 | add_test(NAME unittest.${name} COMMAND unittest-${name}) 21 | endfunction() 22 | 23 | # test the testing framework itself 24 | bkcrack_add_unittest(TestRunner-pass.test.cpp) 25 | bkcrack_add_unittest(TestRunner-fail.test.cpp) 26 | set_property(TEST unittest.testrunner-fail PROPERTY WILL_FAIL ON) 27 | -------------------------------------------------------------------------------- /src/cli/ConsoleProgress.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_CONSOLEPROGRESS_HPP 2 | #define BKCRACK_CONSOLEPROGRESS_HPP 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | /// Progress indicator which prints itself at regular time intervals 11 | class ConsoleProgress : public Progress 12 | { 13 | public: 14 | /// Start a thread to print progress 15 | explicit ConsoleProgress(std::ostream& os, 16 | const std::chrono::milliseconds& interval = std::chrono::milliseconds{200}); 17 | 18 | /// Notify and stop the printing thread 19 | ~ConsoleProgress(); 20 | 21 | private: 22 | const std::chrono::milliseconds m_interval; 23 | 24 | std::mutex m_in_destructor_mutex; 25 | std::condition_variable m_in_destructor_cv; 26 | bool m_in_destructor; 27 | 28 | std::thread m_printer; 29 | void printerFunction(); 30 | }; 31 | 32 | #endif // BKCRACK_CONSOLEPROGRESS_HPP 33 | -------------------------------------------------------------------------------- /src/bkcrack/file.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/file.hpp" 2 | 3 | FileError::FileError(const std::string& description) 4 | : BaseError{"File error", description} 5 | { 6 | } 7 | 8 | auto openInput(const std::string& filename) -> std::ifstream 9 | { 10 | if (auto is = std::ifstream{filename, std::ios::binary}) 11 | return is; 12 | else 13 | throw FileError{"could not open input file " + filename}; 14 | } 15 | 16 | auto loadStream(std::istream& is, std::size_t size) -> std::vector 17 | { 18 | auto content = std::vector{}; 19 | auto it = std::istreambuf_iterator{is}; 20 | for (auto i = std::size_t{}; i < size && it != std::istreambuf_iterator{}; i++, ++it) 21 | content.push_back(*it); 22 | 23 | return content; 24 | } 25 | 26 | auto openOutput(const std::string& filename) -> std::ofstream 27 | { 28 | if (auto os = std::ofstream{filename, std::ios::binary}) 29 | return os; 30 | else 31 | throw FileError{"could not open output file " + filename}; 32 | } 33 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignConsecutiveAssignments: 6 | Enabled: true 7 | AlignConsecutiveDeclarations: 8 | Enabled: true 9 | AllowShortFunctionsOnASingleLine: Empty 10 | AlwaysBreakTemplateDeclarations: Yes 11 | BraceWrapping: 12 | AfterCaseLabel: true 13 | AfterClass: true 14 | AfterControlStatement: Always 15 | AfterEnum: true 16 | AfterFunction: true 17 | AfterNamespace: true 18 | AfterObjCDeclaration: true 19 | AfterStruct: true 20 | AfterUnion: true 21 | AfterExternBlock: true 22 | BeforeCatch: true 23 | BeforeElse: true 24 | BeforeLambdaBody: true 25 | BreakBeforeBraces: Custom 26 | BreakConstructorInitializers: BeforeComma 27 | ColumnLimit: 120 28 | QualifierAlignment: Left 29 | ConstructorInitializerIndentWidth: 0 30 | PackConstructorInitializers: Never 31 | IncludeBlocks: Regroup 32 | IncludeCategories: 33 | - Regex: '".*"' 34 | Priority: 1 35 | - Regex: '' 36 | Priority: 2 37 | - Regex: '' 38 | Priority: 3 39 | - Regex: '<.+>' 40 | Priority: 4 41 | IndentWidth: 4 42 | PointerAlignment: Left 43 | ... 44 | -------------------------------------------------------------------------------- /src/cli/VirtualTerminalSupport.cpp: -------------------------------------------------------------------------------- 1 | #include "VirtualTerminalSupport.hpp" 2 | 3 | #ifdef _WIN32 4 | 5 | #include 6 | #include 7 | 8 | class VirtualTerminalSupport::Impl 9 | { 10 | public: 11 | Impl() 12 | : hStdOut{GetStdHandle(STD_OUTPUT_HANDLE)} 13 | , originalMode{[this] 14 | { 15 | auto mode = DWORD{}; 16 | return GetConsoleMode(hStdOut, &mode) ? std::optional{mode} : std::nullopt; 17 | }()} 18 | { 19 | if (originalMode) 20 | SetConsoleMode(hStdOut, *originalMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); 21 | } 22 | 23 | ~Impl() 24 | { 25 | if (originalMode) 26 | SetConsoleMode(hStdOut, *originalMode); 27 | } 28 | 29 | private: 30 | const HANDLE hStdOut; 31 | const std::optional originalMode; 32 | }; 33 | 34 | #else 35 | 36 | class VirtualTerminalSupport::Impl 37 | { 38 | }; 39 | 40 | #endif // _WIN32 41 | 42 | VirtualTerminalSupport::VirtualTerminalSupport() 43 | : m_impl{std::make_unique()} 44 | { 45 | } 46 | 47 | VirtualTerminalSupport::~VirtualTerminalSupport() = default; 48 | -------------------------------------------------------------------------------- /include/bkcrack/Zreduction.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_ZREDUCTION_HPP 2 | #define BKCRACK_ZREDUCTION_HPP 3 | 4 | #include 5 | #include 6 | 7 | /// Generate and reduce Z values 8 | class Zreduction 9 | { 10 | public: 11 | /// Constructor generating Zi[10,32) values from the last keystream byte 12 | explicit Zreduction(const std::vector& keystream); 13 | 14 | /// Reduce Zi[10,32) number using extra contiguous keystream 15 | void reduce(Progress& progress); 16 | 17 | /// Extend Zi[10,32) values into Zi[2,32) values using keystream 18 | void generate(); 19 | 20 | /// \return the generated Zi[2,32) values 21 | auto getCandidates() const -> const std::vector&; 22 | 23 | /// \return the index of the Zi[2,32) values relative to keystream 24 | auto getIndex() const -> std::size_t; 25 | 26 | private: 27 | const std::vector& keystream; 28 | // After constructor or reduce(), contains Z[10,32) values. 29 | // After generate(), contains Zi[2,32) values. 30 | std::vector zi_vector; 31 | std::size_t index; 32 | }; 33 | 34 | #endif // BKCRACK_ZREDUCTION_HPP 35 | -------------------------------------------------------------------------------- /tests/bkcrack/types.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | TEST("BaseError") 9 | { 10 | const auto error = BaseError{"Type", "description"}; 11 | CHECK(error.what() == std::string_view{"Type: description."}); 12 | } 13 | 14 | TEST("lsb") 15 | { 16 | CHECK(lsb(0x12345678) == 0x78); 17 | } 18 | 19 | TEST("msb") 20 | { 21 | CHECK(msb(0x12345678) == 0x12); 22 | } 23 | 24 | TEST("mask") 25 | { 26 | CHECK(mask<0, 1> == 0x00000001); 27 | CHECK(mask<31, 32> == 0x80000000); 28 | 29 | CHECK(mask<0, 8> == 0x000000ff); 30 | CHECK(mask<8, 16> == 0x0000ff00); 31 | CHECK(mask<16, 24> == 0x00ff0000); 32 | CHECK(mask<24, 32> == 0xff000000); 33 | 34 | CHECK(mask<0, 32> == 0xffffffff); 35 | } 36 | 37 | TEST("maxdiff") 38 | { 39 | auto generator = std::mt19937{}; 40 | auto dist = std::uniform_int_distribution{}; 41 | 42 | for (auto i = 0; i < 1'000; ++i) 43 | { 44 | const auto b = dist(generator); 45 | for (auto byte = 0; byte < 256; ++byte) 46 | { 47 | const auto a = b + byte; 48 | CHECK(a - (b & mask<24, 32>) <= maxdiff<24>); 49 | CHECK(a - (b & mask<26, 32>) <= maxdiff<26>); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /include/bkcrack/Progress.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_PROGRESS_HPP 2 | #define BKCRACK_PROGRESS_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /// Structure to report the progress of a long operation or to cancel it 9 | class Progress 10 | { 11 | public: 12 | /// Possible states of a long operation 13 | enum class State 14 | { 15 | Normal, ///< The operation is ongoing or is fully completed 16 | Canceled, ///< The operation has been canceled externally 17 | EarlyExit ///< The operation stopped after a partial result was found 18 | }; 19 | 20 | /// Constructor 21 | explicit Progress(std::ostream& os); 22 | 23 | /// Get exclusive access to the shared output stream and output progress 24 | /// information with the given function 25 | template 26 | void log(F f) 27 | { 28 | const auto lock = std::scoped_lock{m_os_mutex}; 29 | f(m_os); 30 | } 31 | 32 | std::atomic state = State::Normal; ///< State of the long operation 33 | std::atomic done = 0; ///< Number of steps already done 34 | std::atomic total = 0; ///< Total number of steps 35 | 36 | private: 37 | std::mutex m_os_mutex; 38 | std::ostream& m_os; 39 | }; 40 | 41 | #endif // BKCRACK_PROGRESS_HPP 42 | -------------------------------------------------------------------------------- /tests/bkcrack/MultTab.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | TEST("getMsbProdFiber2") 8 | { 9 | for (auto msbprodinv = 0; msbprodinv < 256; ++msbprodinv) 10 | { 11 | const auto fiber = MultTab::getMsbProdFiber2(msbprodinv); 12 | for (auto x = 0; x < 256; ++x) 13 | { 14 | const auto m = msb(x * MultTab::multInv); 15 | const auto expectedInFiber = m == msbprodinv || m == (msbprodinv + 255) % 256; 16 | const auto actuallyInFiber = std::find(fiber.begin(), fiber.end(), x) != fiber.end(); 17 | CHECK(actuallyInFiber == expectedInFiber); 18 | } 19 | } 20 | } 21 | 22 | TEST("getMsbProdFiber3") 23 | { 24 | for (auto msbprodinv = 0; msbprodinv < 256; ++msbprodinv) 25 | { 26 | const auto fiber = MultTab::getMsbProdFiber3(msbprodinv); 27 | for (auto x = 0; x < 256; ++x) 28 | { 29 | const auto m = msb(x * MultTab::multInv); 30 | const auto expectedInFiber = 31 | m == msbprodinv || m == (msbprodinv + 255) % 256 || m == (msbprodinv + 1) % 256; 32 | const auto actuallyInFiber = std::find(fiber.begin(), fiber.end(), x) != fiber.end(); 33 | CHECK(actuallyInFiber == expectedInFiber); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /include/bkcrack/Crc32Tab.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_CRC32TAB_HPP 2 | #define BKCRACK_CRC32TAB_HPP 3 | 4 | #include 5 | 6 | /// Lookup tables for CRC32 related computations 7 | class Crc32Tab 8 | { 9 | public: 10 | /// \return CRC32 using a lookup table 11 | static auto crc32(std::uint32_t pval, std::uint8_t b) -> std::uint32_t 12 | { 13 | return pval >> 8 ^ instance.crctab[lsb(pval) ^ b]; 14 | } 15 | 16 | /// \return CRC32^-1 using a lookup table 17 | static auto crc32inv(std::uint32_t crc, std::uint8_t b) -> std::uint32_t 18 | { 19 | return crc << 8 ^ instance.crcinvtab[msb(crc)] ^ b; 20 | } 21 | 22 | /// \return Yi[24,32) from Zi and Z{i-1} using CRC32^-1 23 | static auto getYi_24_32(std::uint32_t zi, std::uint32_t zim1) -> std::uint32_t 24 | { 25 | return (crc32inv(zi, 0) ^ zim1) << 24; 26 | } 27 | 28 | /// \return Z{i-1}[10,32) from Zi[2,32) using CRC32^-1 29 | static auto getZim1_10_32(std::uint32_t zi_2_32) -> std::uint32_t 30 | { 31 | return crc32inv(zi_2_32, 0) & mask<10, 32>; // discard 10 least significant bits 32 | } 33 | 34 | private: 35 | // initialize lookup tables 36 | Crc32Tab(); 37 | 38 | // lookup tables 39 | std::array crctab; 40 | std::array crcinvtab; 41 | 42 | static const Crc32Tab instance; 43 | }; 44 | 45 | #endif // BKCRACK_CRC32TAB_HPP 46 | -------------------------------------------------------------------------------- /include/bkcrack/MultTab.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_MULTTAB_HPP 2 | #define BKCRACK_MULTTAB_HPP 3 | 4 | #include 5 | 6 | /// Lookup tables for multiplication related computations 7 | class MultTab 8 | { 9 | public: 10 | /// \return a vector of bytes x such that 11 | /// msb(x*mult^-1) is equal to msbprod or msbprod-1 12 | static auto getMsbProdFiber2(std::uint8_t msbprodinv) -> const std::vector& 13 | { 14 | return instance.msbprodfiber2[msbprodinv]; 15 | } 16 | 17 | /// \return a vector of bytes x such that 18 | /// msb(x*mult^-1) is equal to msbprod, msbprod-1 or msbprod+1 19 | static auto getMsbProdFiber3(std::uint8_t msbprodinv) -> const std::vector& 20 | { 21 | return instance.msbprodfiber3[msbprodinv]; 22 | } 23 | 24 | /// Multiplicative constant used in traditional PKWARE encryption 25 | static constexpr std::uint32_t mult = 0x08088405; 26 | 27 | /// Multiplicative inverse of mult modulo 2^32 28 | static constexpr std::uint32_t multInv = 0xd94fa8cd; 29 | static_assert(mult * multInv == 1); 30 | 31 | private: 32 | // initialize lookup tables 33 | MultTab(); 34 | 35 | // lookup tables 36 | std::array, 256> msbprodfiber2; 37 | std::array, 256> msbprodfiber3; 38 | 39 | static const MultTab instance; 40 | }; 41 | 42 | #endif // BKCRACK_MULTTAB_HPP 43 | -------------------------------------------------------------------------------- /tests/bkcrack/KeystreamTab.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace 8 | { 9 | constexpr auto k = std::array{ 10 | 't' ^ 0x9a, 11 | 'e' ^ 0x6b, 12 | 's' ^ 0x40, 13 | 't' ^ 0x2c, 14 | }; 15 | } 16 | 17 | TEST("getByte") 18 | { 19 | CHECK(KeystreamTab::getByte(0x5ff8707d) == k[0]); 20 | CHECK(KeystreamTab::getByte(0x868c2aa4) == k[1]); 21 | CHECK(KeystreamTab::getByte(0x2d8463a7) == k[2]); 22 | CHECK(KeystreamTab::getByte(0x23f4e3dc) == k[3]); 23 | } 24 | 25 | TEST("getZi_2_16_vector") 26 | { 27 | CHECK(KeystreamTab::getZi_2_16_vector(k[0], 0x7000) == std::vector{0x707c}); 28 | CHECK(KeystreamTab::getZi_2_16_vector(k[1], 0x2800) == std::vector{0x29a8, 0x2aa4, 0x2ab0, 0x2b3c}); 29 | CHECK(KeystreamTab::getZi_2_16_vector(k[2], 0x6000) == std::vector{0x6090, 0x6184, 0x63a4}); 30 | CHECK(KeystreamTab::getZi_2_16_vector(k[3], 0xe000) == std::vector{0xe3dc}); 31 | 32 | CHECK(KeystreamTab::getZi_2_16_vector(k[0], 0x6000) == std::vector{}); 33 | } 34 | 35 | TEST("hasZi_2_16") 36 | { 37 | for (auto ki = 0; ki < 256; ++ki) 38 | for (auto zi_10_16 = 0; zi_10_16 < (1 << 16); zi_10_16 += 1 << 10) 39 | CHECK(KeystreamTab::hasZi_2_16(ki, zi_10_16) == !KeystreamTab::getZi_2_16_vector(ki, zi_10_16).empty()); 40 | } 41 | -------------------------------------------------------------------------------- /include/bkcrack/types.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_TYPES_HPP 2 | #define BKCRACK_TYPES_HPP 3 | 4 | /// \file types.hpp 5 | /// \brief Useful types, constants and utility functions 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /// Base exception type 14 | class BaseError : public std::runtime_error 15 | { 16 | public: 17 | /// Constructor 18 | explicit BaseError(const std::string& type, const std::string& description); 19 | }; 20 | 21 | // utility functions 22 | 23 | /// \return the least significant byte of x 24 | constexpr auto lsb(std::uint32_t x) -> std::uint8_t 25 | { 26 | return x; 27 | } 28 | 29 | /// \return the most significant byte of x 30 | constexpr auto msb(std::uint32_t x) -> std::uint8_t 31 | { 32 | return x >> 24; 33 | } 34 | 35 | // constants 36 | 37 | /// Constant value for bit masking 38 | template 39 | constexpr auto mask = std::uint32_t{~0u << begin & ~0u >> (32 - end)}; 40 | 41 | /// \brief Maximum difference between 32-bits integers A and B[x,32) 42 | /// knowing that A = B + b and b is a byte. 43 | /// 44 | /// The following equations show how the difference is bounded by the given constants: 45 | /// 46 | /// A = B + b 47 | /// A = B[0,x) + B[x,32) + b 48 | /// A - B[x,32) = B[0,x) + b 49 | /// A - B[x,32) <= 0xffffffff[0,x) + 0xff 50 | template 51 | constexpr auto maxdiff = std::uint32_t{mask<0, x> + 0xff}; 52 | 53 | #endif // BKCRACK_TYPES_HPP 54 | -------------------------------------------------------------------------------- /include/bkcrack/KeystreamTab.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_KEYSTREAMTAB_HPP 2 | #define BKCRACK_KEYSTREAMTAB_HPP 3 | 4 | #include 5 | 6 | #include 7 | 8 | /// Lookup tables for keystream related computations 9 | class KeystreamTab 10 | { 11 | public: 12 | /// \return the keystream byte ki associated to a Zi value 13 | /// \note Only Zi[2,16) is used 14 | static auto getByte(std::uint32_t zi) -> std::uint8_t 15 | { 16 | return instance.keystreamtab[(zi & mask<0, 16>) >> 2]; 17 | } 18 | 19 | /// \return a vector of Zi[2,16) values having given [10,16) bits 20 | /// such that getByte(zi) is equal to ki 21 | /// \note the vector contains one element on average 22 | static auto getZi_2_16_vector(std::uint8_t ki, std::uint32_t zi_10_16) -> const std::vector& 23 | { 24 | return instance.keystreaminvfiltertab[ki][(zi_10_16 & mask<0, 16>) >> 10]; 25 | } 26 | 27 | /// \return true if the vector returned by getZi_2_16_vector is not empty, 28 | /// false otherwise 29 | static auto hasZi_2_16(std::uint8_t ki, std::uint32_t zi_10_16) -> bool 30 | { 31 | return instance.keystreaminvexists[ki][(zi_10_16 & mask<0, 16>) >> 10]; 32 | } 33 | 34 | private: 35 | // initialize lookup tables 36 | KeystreamTab(); 37 | 38 | // lookup tables 39 | std::array keystreamtab; 40 | std::array, 64>, 256> keystreaminvfiltertab; 41 | std::array, 256> keystreaminvexists; 42 | 43 | static const KeystreamTab instance; 44 | }; 45 | 46 | #endif // BKCRACK_KEYSTREAMTAB_HPP 47 | -------------------------------------------------------------------------------- /tests/bkcrack/file.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | TEST("FileError") 10 | { 11 | const auto error = FileError{"description"}; 12 | CHECK(error.what() == std::string_view{"File error: description."}); 13 | } 14 | 15 | TEST("openInput success") 16 | { 17 | CHECK(openInput(__FILE__)); 18 | } 19 | 20 | TEST("openInput failure") 21 | { 22 | CHECK_THROWS(FileError, "could not open input file does-not-exist.txt", openInput("does-not-exist.txt")); 23 | } 24 | 25 | TEST("loadStream") 26 | { 27 | auto is = std::istringstream{"Hello World!"}; 28 | 29 | auto data = loadStream(is, 5); 30 | CHECK(data == std::vector{'H', 'e', 'l', 'l', 'o'}); 31 | 32 | data = loadStream(is, 5); 33 | CHECK(data == std::vector{' ', 'W', 'o', 'r', 'l'}); 34 | 35 | data = loadStream(is, 5); 36 | CHECK(data == std::vector{'d', '!'}); 37 | } 38 | 39 | TEST("openOutput success") 40 | { 41 | static constexpr auto testFilename = "file.test.txt"; 42 | struct Cleaner 43 | { 44 | ~Cleaner() 45 | { 46 | std::filesystem::remove(testFilename); 47 | } 48 | }; 49 | 50 | CHECK(!std::filesystem::exists(testFilename)); 51 | const auto cleaner = Cleaner{}; 52 | 53 | CHECK(openOutput(testFilename) << "Hello World!"); 54 | CHECK(std::filesystem::exists(testFilename)); 55 | 56 | auto is = openInput(testFilename); 57 | CHECK(loadStream(is, 5) == std::vector{'H', 'e', 'l', 'l', 'o'}); 58 | } 59 | 60 | TEST("openOutput failure") 61 | { 62 | CHECK_THROWS(FileError, "could not open output file missing/folder/test.txt", 63 | openOutput("missing/folder/test.txt")); 64 | } 65 | -------------------------------------------------------------------------------- /include/bkcrack/Data.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_DATA_HPP 2 | #define BKCRACK_DATA_HPP 3 | 4 | #include 5 | 6 | #include 7 | 8 | /// Structure to hold the data needed for an attack 9 | struct Data 10 | { 11 | /// Size of the traditional PKWARE encryption header 12 | static constexpr std::size_t encryptionHeaderSize = 12; 13 | 14 | /// Exception thrown if data cannot be used to carry out an attack 15 | class Error : public BaseError 16 | { 17 | public: 18 | /// Constructor 19 | explicit Error(const std::string& description); 20 | }; 21 | 22 | /// \brief Construct data and check it can be used to carry out an attack 23 | /// \param ciphertext Ciphertext bytes including encryption header 24 | /// \param plaintext Plaintext bytes 25 | /// \param offset Plaintext offset relative to ciphertext without encryption header (may be negative) 26 | /// \param extraPlaintext Additional bytes of plaintext with their offset relative to ciphertext without encryption 27 | /// header (may be negative) 28 | /// \exception Error if the given data cannot be used to carry out an attack 29 | Data(std::vector ciphertext, std::vector plaintext, int offset, 30 | const std::map& extraPlaintext); 31 | 32 | std::vector ciphertext; ///< ciphertext bytes including encryption header 33 | std::vector plaintext; ///< plaintext bytes 34 | std::vector keystream; ///< keystream bytes 35 | 36 | /// plaintext and keystream offset relative to ciphertext with encryption header 37 | std::size_t offset; 38 | 39 | /// additional bytes of plaintext with their offset relative to ciphertext with encryption header 40 | std::vector> extraPlaintext; 41 | }; 42 | 43 | #endif // BKCRACK_DATA_HPP 44 | -------------------------------------------------------------------------------- /tests/bkcrack/Crc32Tab.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | TEST("crc32") 8 | { 9 | CHECK(Crc32Tab::crc32(0x00000000, 0x00) == 0x00000000); 10 | CHECK(Crc32Tab::crc32(0x12345678, 0x00) == 0x5ecccd58); 11 | CHECK(Crc32Tab::crc32(0x12345678, 0x42) == 0xc61eede4); 12 | CHECK(Crc32Tab::crc32(0x12345678, 0xff) == 0x73ce22d5); 13 | CHECK(Crc32Tab::crc32(0xffffffff, 0xff) == 0x00ffffff); 14 | } 15 | 16 | TEST("crc32inv") 17 | { 18 | CHECK(Crc32Tab::crc32inv(0x00000000, 0x00) == 0x00000000); 19 | CHECK(Crc32Tab::crc32inv(0x5ecccd58, 0x00) == 0x12345678); 20 | CHECK(Crc32Tab::crc32inv(0xc61eede4, 0x42) == 0x12345678); 21 | CHECK(Crc32Tab::crc32inv(0x73ce22d5, 0xff) == 0x12345678); 22 | CHECK(Crc32Tab::crc32inv(0x00ffffff, 0xff) == 0xffffffff); 23 | } 24 | 25 | TEST("getYi_24_32") 26 | { 27 | CHECK(Crc32Tab::getYi_24_32(0x00000000, 0x00000000) == 0x00000000); 28 | CHECK(Crc32Tab::getYi_24_32(0x5ecccd58, 0x12345678) == 0x00000000); 29 | CHECK(Crc32Tab::getYi_24_32(0xc61eede4, 0x12345678) == 0x42000000); 30 | CHECK(Crc32Tab::getYi_24_32(0x73ce22d5, 0x12345678) == 0xff000000); 31 | CHECK(Crc32Tab::getYi_24_32(0x00ffffff, 0xffffffff) == 0xff000000); 32 | } 33 | 34 | TEST("getZim1_10_32") 35 | { 36 | CHECK(Crc32Tab::getZim1_10_32(0x00000000) == 0x00000000); 37 | CHECK(Crc32Tab::getZim1_10_32(0x5ecccd58) == 0x12345400); 38 | CHECK(Crc32Tab::getZim1_10_32(0xc61eede4) == 0x12345400); 39 | CHECK(Crc32Tab::getZim1_10_32(0x73ce22d5) == 0x12345400); 40 | CHECK(Crc32Tab::getZim1_10_32(0x00ffffff) == 0xfffffc00); 41 | } 42 | 43 | TEST("compute message CRC-32") 44 | { 45 | const auto message = std::string_view{"Hello World!"}; 46 | 47 | auto crc = 0xffffffff; 48 | for (const auto byte : message) 49 | crc = Crc32Tab::crc32(crc, byte); 50 | crc ^= 0xffffffff; 51 | 52 | CHECK(crc == 0x1c291ca3); 53 | } 54 | -------------------------------------------------------------------------------- /include/bkcrack/Keys.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_KEYS_HPP 2 | #define BKCRACK_KEYS_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /// Keys defining the cipher state 9 | class Keys 10 | { 11 | public: 12 | /// Construct default state 13 | Keys() = default; 14 | 15 | /// Construct keys from given components 16 | Keys(std::uint32_t x, std::uint32_t y, std::uint32_t z); 17 | 18 | /// Construct keys associated to the given password 19 | explicit Keys(const std::string& password); 20 | 21 | /// Update the state with a plaintext byte 22 | void update(std::uint8_t p) 23 | { 24 | x = Crc32Tab::crc32(x, p); 25 | y = (y + lsb(x)) * MultTab::mult + 1; 26 | z = Crc32Tab::crc32(z, msb(y)); 27 | } 28 | 29 | /// Update the state forward to a target offset 30 | void update(const std::vector& ciphertext, std::size_t current, std::size_t target); 31 | 32 | /// Update the state backward with a ciphertext byte 33 | void updateBackward(std::uint8_t c) 34 | { 35 | z = Crc32Tab::crc32inv(z, msb(y)); 36 | y = (y - 1) * MultTab::multInv - lsb(x); 37 | x = Crc32Tab::crc32inv(x, c ^ getK()); 38 | } 39 | 40 | /// Update the state backward with a plaintext byte 41 | void updateBackwardPlaintext(std::uint8_t p) 42 | { 43 | z = Crc32Tab::crc32inv(z, msb(y)); 44 | y = (y - 1) * MultTab::multInv - lsb(x); 45 | x = Crc32Tab::crc32inv(x, p); 46 | } 47 | 48 | /// Update the state backward to a target offset 49 | void updateBackward(const std::vector& ciphertext, std::size_t current, std::size_t target); 50 | 51 | /// \return X value 52 | auto getX() const -> std::uint32_t 53 | { 54 | return x; 55 | } 56 | 57 | /// \return Y value 58 | auto getY() const -> std::uint32_t 59 | { 60 | return y; 61 | } 62 | 63 | /// \return Z value 64 | auto getZ() const -> std::uint32_t 65 | { 66 | return z; 67 | } 68 | 69 | /// \return the keystream byte derived from the keys 70 | auto getK() const -> std::uint8_t 71 | { 72 | return KeystreamTab::getByte(z); 73 | } 74 | 75 | private: 76 | std::uint32_t x = 0x12345678; 77 | std::uint32_t y = 0x23456789; 78 | std::uint32_t z = 0x34567890; 79 | }; 80 | 81 | #endif // BKCRACK_KEYS_HPP 82 | -------------------------------------------------------------------------------- /src/cli/ConsoleProgress.cpp: -------------------------------------------------------------------------------- 1 | #include "ConsoleProgress.hpp" 2 | 3 | #include 4 | 5 | ConsoleProgress::ConsoleProgress(std::ostream& os, const std::chrono::milliseconds& interval) 6 | : Progress{os} 7 | , m_interval{interval} 8 | , m_in_destructor{false} 9 | , m_printer{&ConsoleProgress::printerFunction, this} 10 | { 11 | } 12 | 13 | ConsoleProgress::~ConsoleProgress() 14 | { 15 | { 16 | const auto lock = std::scoped_lock{m_in_destructor_mutex}; 17 | m_in_destructor = true; 18 | } 19 | 20 | m_in_destructor_cv.notify_all(); 21 | m_printer.join(); 22 | } 23 | 24 | void ConsoleProgress::printerFunction() 25 | { 26 | auto repeat = true; 27 | 28 | // Give a small delay before the first time progress is printed so that 29 | // the running operation is likely to have initialized the total number of steps. 30 | { 31 | auto lock = std::unique_lock{m_in_destructor_mutex}; 32 | repeat = !m_in_destructor_cv.wait_for(lock, std::chrono::milliseconds{1}, [this] { return m_in_destructor; }); 33 | } 34 | 35 | while (repeat) 36 | { 37 | if (const auto total = this->total.load()) 38 | log( 39 | [done = done.load(), total](std::ostream& os) 40 | { 41 | const auto flagsBefore = os.setf(std::ios::fixed, std::ios::floatfield); 42 | const auto precisionBefore = os.precision(1); 43 | 44 | os << (100.0 * done / total) << " % (" << done << " / " << total << ")" << std::flush 45 | << "\033[1K\r"; 46 | 47 | os.precision(precisionBefore); 48 | os.flags(flagsBefore); 49 | }); 50 | 51 | auto lock = std::unique_lock{m_in_destructor_mutex}; 52 | repeat = !m_in_destructor_cv.wait_for(lock, m_interval, [this] { return m_in_destructor; }); 53 | } 54 | 55 | if (const auto total = this->total.load()) 56 | log( 57 | [done = done.load(), total](std::ostream& os) 58 | { 59 | const auto flagsBefore = os.setf(std::ios::fixed, std::ios::floatfield); 60 | const auto precisionBefore = os.precision(1); 61 | 62 | os << (100.0 * done / total) << " % (" << done << " / " << total << ")" << std::endl; 63 | 64 | os.precision(precisionBefore); 65 | os.flags(flagsBefore); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /tests/runner/TestRunner.cpp: -------------------------------------------------------------------------------- 1 | #include "TestRunner.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace 9 | { 10 | 11 | struct TestCase 12 | { 13 | const char* name; 14 | void (&function)(); 15 | }; 16 | 17 | auto testRegistry() -> std::vector& 18 | { 19 | static auto tests = std::vector{}; 20 | return tests; 21 | } 22 | 23 | } // namespace 24 | 25 | auto checkMessageContains(const char* actual, const char* expected) -> bool 26 | { 27 | return std::string_view{actual}.find(expected) != std::string_view::npos; 28 | } 29 | 30 | TestRegistration::TestRegistration(const char* name, void (&function)()) 31 | { 32 | testRegistry().emplace_back(name, function); 33 | } 34 | 35 | auto TestRunner::runAllTests() -> bool 36 | { 37 | auto pass = 0; 38 | auto fail = 0; 39 | 40 | const auto allStart = std::chrono::high_resolution_clock::now(); 41 | for (const auto& [name, function] : testRegistry()) 42 | { 43 | std::cout << name << std::flush; 44 | const auto start = std::chrono::high_resolution_clock::now(); 45 | try 46 | { 47 | function(); 48 | std::cout << " [PASS]"; 49 | ++pass; 50 | } 51 | catch (const TestError& error) 52 | { 53 | std::cout << "\n " << error.expression << " [FAIL]"; 54 | ++fail; 55 | } 56 | catch (const std::exception& error) 57 | { 58 | std::cout << "\n " << error.what() << " [FAIL]"; 59 | ++fail; 60 | } 61 | catch (...) 62 | { 63 | std::cout << "\n exception [FAIL]"; 64 | ++fail; 65 | } 66 | const auto end = std::chrono::high_resolution_clock::now(); 67 | if (const auto duration = end - start; duration > std::chrono::milliseconds{1}) 68 | std::cout << " (" << std::chrono::duration_cast(duration).count() << " ms)"; 69 | std::cout << std::endl; 70 | } 71 | 72 | std::cout << "Tests: " << pass << " pass, " << fail << " fail"; 73 | const auto allEnd = std::chrono::high_resolution_clock::now(); 74 | if (const auto duration = allEnd - allStart; duration > std::chrono::milliseconds{1}) 75 | std::cout << " (" << std::chrono::duration_cast(duration).count() << " ms)"; 76 | std::cout << std::endl; 77 | 78 | return !fail; 79 | } 80 | -------------------------------------------------------------------------------- /include/bkcrack/password.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_PASSWORD_HPP 2 | #define BKCRACK_PASSWORD_HPP 3 | 4 | #include 5 | #include 6 | 7 | /// \file password.hpp 8 | 9 | /// \brief Try to recover the password associated with the given keys 10 | /// \param keys Internal keys for which a password is wanted 11 | /// \param charset The set of characters with which to constitute password candidates 12 | /// \param minLength The smallest password length to try 13 | /// \param maxLength The greatest password length to try 14 | /// \param start Starting point in the password search space. 15 | /// Also used as an output parameter to tell where to restart. 16 | /// \param jobs Number of threads to use 17 | /// \param exhaustive True to try and find all valid passwords, 18 | /// false to stop searching after the first one is found 19 | /// \param progress Object to report progress 20 | /// \return A vector of passwords associated with the given keys. 21 | /// A vector is needed instead of a single string because there can be 22 | /// collisions (i.e. several passwords for the same keys). 23 | auto recoverPassword(const Keys& keys, const std::vector& charset, std::size_t minLength, 24 | std::size_t maxLength, std::string& start, int jobs, bool exhaustive, Progress& progress) 25 | -> std::vector; 26 | 27 | /// \brief Try to recover the password associated with the given keys 28 | /// \param keys Internal keys for which a password is wanted 29 | /// \param mask A sequence of character sets, each corresponding to the successive characters of password candidates 30 | /// \param start Starting point in the password search space. 31 | /// Also used as an output parameter to tell where to restart. 32 | /// \param jobs Number of threads to use 33 | /// \param exhaustive True to try and find all valid passwords, 34 | /// false to stop searching after the first one is found 35 | /// \param progress Object to report progress 36 | /// \return A vector of passwords associated with the given keys. 37 | /// A vector is needed instead of a single string because there can be 38 | /// collisions (i.e. several passwords for the same keys). 39 | auto recoverPassword(const Keys& keys, const std::vector>& mask, std::string& start, int jobs, 40 | bool exhaustive, Progress& progress) -> std::vector; 41 | 42 | #endif // BKCRACK_PASSWORD_HPP 43 | -------------------------------------------------------------------------------- /tests/bkcrack/Keys.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | TEST("default constructor") 6 | { 7 | const auto keys = Keys{}; 8 | CHECK(keys.getX() == 0x12345678); 9 | CHECK(keys.getY() == 0x23456789); 10 | CHECK(keys.getZ() == 0x34567890); 11 | } 12 | 13 | TEST("construct from components") 14 | { 15 | const auto keys = Keys{0xea9b4e4d, 0xba789085, 0x5ff8707d}; 16 | CHECK(keys.getX() == 0xea9b4e4d); 17 | CHECK(keys.getY() == 0xba789085); 18 | CHECK(keys.getZ() == 0x5ff8707d); 19 | } 20 | 21 | TEST("construct from password") 22 | { 23 | const auto keys = Keys{"password"}; 24 | CHECK(keys.getX() == 0xea9b4e4d); 25 | CHECK(keys.getY() == 0xba789085); 26 | CHECK(keys.getZ() == 0x5ff8707d); 27 | } 28 | 29 | TEST("update forward with plaintext bytes") 30 | { 31 | auto keys = Keys{"password"}; 32 | keys.update('t'); 33 | keys.update('e'); 34 | keys.update('s'); 35 | keys.update('t'); 36 | 37 | CHECK(keys.getX() == 0x382bd98d); 38 | CHECK(keys.getY() == 0x5ad55f3b); 39 | CHECK(keys.getZ() == 0x04f8d2f6); 40 | } 41 | 42 | TEST("update forward with ciphertext") 43 | { 44 | const auto ciphertext = std::vector{0x9a, 0x6b, 0x40, 0x2c}; 45 | 46 | auto keys = Keys{"password"}; 47 | keys.update(ciphertext, 0, 4); 48 | 49 | CHECK(keys.getX() == 0x382bd98d); 50 | CHECK(keys.getY() == 0x5ad55f3b); 51 | CHECK(keys.getZ() == 0x04f8d2f6); 52 | } 53 | 54 | TEST("update backward with ciphertext bytes") 55 | { 56 | auto keys = Keys{0x382bd98d, 0x5ad55f3b, 0x04f8d2f6}; 57 | keys.updateBackward(0x2c); 58 | keys.updateBackward(0x40); 59 | keys.updateBackward(0x6b); 60 | keys.updateBackward(0x9a); 61 | 62 | CHECK(keys.getX() == 0xea9b4e4d); 63 | CHECK(keys.getY() == 0xba789085); 64 | CHECK(keys.getZ() == 0x5ff8707d); 65 | } 66 | 67 | TEST("update backward with plaintext bytes") 68 | { 69 | auto keys = Keys{0x382bd98d, 0x5ad55f3b, 0x04f8d2f6}; 70 | keys.updateBackwardPlaintext('t'); 71 | keys.updateBackwardPlaintext('s'); 72 | keys.updateBackwardPlaintext('e'); 73 | keys.updateBackwardPlaintext('t'); 74 | 75 | CHECK(keys.getX() == 0xea9b4e4d); 76 | CHECK(keys.getY() == 0xba789085); 77 | CHECK(keys.getZ() == 0x5ff8707d); 78 | } 79 | 80 | TEST("update backward with ciphertext") 81 | { 82 | const auto ciphertext = std::vector{0x9a, 0x6b, 0x40, 0x2c}; 83 | 84 | auto keys = Keys{0x382bd98d, 0x5ad55f3b, 0x04f8d2f6}; 85 | keys.updateBackward(ciphertext, 4, 0); 86 | 87 | CHECK(keys.getX() == 0xea9b4e4d); 88 | CHECK(keys.getY() == 0xba789085); 89 | CHECK(keys.getZ() == 0x5ff8707d); 90 | } 91 | -------------------------------------------------------------------------------- /tools/deflate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import zlib 5 | import argparse 6 | 7 | def deflate(data, level=-1, wbits=-zlib.MAX_WBITS, strategy=zlib.Z_DEFAULT_STRATEGY): 8 | """Returns compressed data.""" 9 | compressor = zlib.compressobj(level, zlib.DEFLATED, wbits, zlib.DEF_MEM_LEVEL, strategy) 10 | return compressor.compress(data) + compressor.flush() 11 | 12 | def main(): 13 | """Read uncompressed data from stdin and write deflated data to stdout.""" 14 | 15 | # Strategies are described in the documentation of the deflateInit2 function in zlib's manual. 16 | # See: https://www.zlib.net/manual.html#Advanced 17 | zlib_strategies = { 18 | 'default': zlib.Z_DEFAULT_STRATEGY, 19 | 'filtered': zlib.Z_FILTERED, 20 | 'huffman_only': zlib.Z_HUFFMAN_ONLY, 21 | 'rle': zlib.Z_RLE, 22 | 'fixed': zlib.Z_FIXED 23 | } 24 | 25 | parser = argparse.ArgumentParser(description='Deflate stdin to stdout') 26 | 27 | parser.add_argument('-l', '--level', 28 | metavar='LEVEL', 29 | type=int, 30 | choices=range(-1, 9 + 1), 31 | help='Compression level (0..9 or -1 for default)', 32 | default=-1) 33 | 34 | parser.add_argument('-w', '--wsize', 35 | metavar='WSIZE', 36 | type=int, 37 | choices=range(9, 15 + 1), 38 | help='Base-two logarithm of the window size (9..15)', 39 | default=zlib.MAX_WBITS) 40 | 41 | parser.add_argument('-s', '--strategy', 42 | metavar='STRATEGY', 43 | choices=zlib_strategies.keys(), 44 | help=f"""Strategy to tune the compression algorithm (choose from '{"', '".join(zlib_strategies)}')""", 45 | default='default') 46 | 47 | parser.add_argument('-z', '--zlib', 48 | action='store_true', 49 | help='Add zlib header and trailer to output. ' 50 | 'This option is available for the completeness of this script. ' 51 | 'ZIP files use raw deflate, so do not enable this option ' 52 | 'if you need compressed plaintext for bkcrack.') 53 | 54 | args = parser.parse_args() 55 | 56 | if args.zlib: 57 | wbits = args.wsize 58 | else: 59 | wbits = -args.wsize 60 | sys.stdout.buffer.write(deflate(sys.stdin.buffer.read(), args.level, wbits, zlib_strategies[args.strategy])) 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /include/bkcrack/Attack.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_ATTACK_HPP 2 | #define BKCRACK_ATTACK_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | /// \file Attack.hpp 12 | 13 | /// Class to carry out the attack for a given Z[2,32) value 14 | class Attack 15 | { 16 | public: 17 | /// \brief Constructor 18 | /// \param data Data used to carry out the attack 19 | /// \param index Index of Z[2,32) values passed to carry out the attack 20 | /// \param solutions Shared output vector for valid keys 21 | /// \param solutionsMutex Mutex to protect \a solutions from concurrent access 22 | /// \param exhaustive True to try and find all valid keys, 23 | /// false to stop searching after the first one is found 24 | /// \param progress Object to report progress 25 | Attack(const Data& data, std::size_t index, std::vector& solutions, std::mutex& solutionsMutex, 26 | bool exhaustive, Progress& progress); 27 | 28 | /// Carry out the attack for the given Z[2,32) value 29 | void carryout(std::uint32_t z7_2_32); 30 | 31 | /// Number of contiguous known plaintext bytes required by the attack 32 | static constexpr std::size_t contiguousSize = 8; 33 | 34 | /// Total number of known plaintext bytes required by the attack 35 | static constexpr std::size_t attackSize = 12; 36 | 37 | private: 38 | // iterate recursively over Z-lists 39 | void exploreZlists(int i); 40 | 41 | // iterate recursively over Y-lists 42 | void exploreYlists(int i); 43 | 44 | // check whether the X-list is valid or not 45 | void testXlist(); 46 | 47 | const Data& data; 48 | 49 | const std::size_t index; // starting index of the used plaintext and keystream 50 | 51 | std::vector& solutions; // shared output vector of valid keys 52 | std::mutex& solutionsMutex; 53 | const bool exhaustive; 54 | Progress& progress; 55 | 56 | std::array zlist; 57 | std::array ylist; // the first two elements are not used 58 | std::array xlist; // the first four elements are not used 59 | }; 60 | 61 | /// \brief Iterate on Zi[2,32) candidates to try and find complete internal keys 62 | /// \param data Data used to carry out the attack 63 | /// \param zi_2_32_vector Zi[2,32) candidates 64 | /// \param start Starting index of Zi[2,32) candidates in zi_2_32_vector to try. 65 | /// Also used as an output parameter to tell where to restart. 66 | /// \param index Index of the Zi[2,32) values relative to keystream 67 | /// \param jobs Number of threads to use 68 | /// \param exhaustive True to try and find all valid keys, 69 | /// false to stop searching after the first one is found 70 | /// \param progress Object to report progress 71 | auto attack(const Data& data, const std::vector& zi_2_32_vector, int& start, std::size_t index, int jobs, 72 | bool exhaustive, Progress& progress) -> std::vector; 73 | 74 | #endif // BKCRACK_ATTACK_HPP 75 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.23) 2 | 3 | # project definition 4 | project(bkcrack 5 | VERSION 1.8.1 # remember to update bkcrack_VERSION_DATE below when releasing a new version 6 | DESCRIPTION "Crack legacy zip encryption with Biham and Kocher's known plaintext attack." 7 | HOMEPAGE_URL "https://github.com/kimci86/bkcrack" 8 | LANGUAGES CXX) 9 | set(bkcrack_VERSION_DATE "2025-10-25") 10 | 11 | # default build type 12 | set(default_build_type "Release") 13 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 14 | message(STATUS "Setting build type to '${default_build_type}' as none was specified.") 15 | set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE) 16 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 17 | endif() 18 | 19 | # function to enable compiler warnings on given target 20 | function(target_enable_warnings target) 21 | target_compile_options(${target} PRIVATE $<$:-Wall -Wextra -Wpedantic>) 22 | endfunction() 23 | 24 | # core library 25 | add_subdirectory(src/bkcrack) 26 | 27 | # main program 28 | add_subdirectory(src/cli) 29 | 30 | # documentation generation 31 | option(BKCRACK_BUILD_DOC "Enable documentation generation with doxygen." OFF) 32 | if(BKCRACK_BUILD_DOC) 33 | add_subdirectory(doc) 34 | endif() 35 | 36 | # automated tests 37 | option(BKCRACK_BUILD_TESTING "Enable automated testing with ctest." OFF) 38 | if(BKCRACK_BUILD_TESTING) 39 | enable_testing() 40 | add_subdirectory(tests) 41 | endif() 42 | 43 | # code coverage report 44 | option(BKCRACK_BUILD_COVERAGE "Enable code coverage report generation." OFF) 45 | if(BKCRACK_BUILD_COVERAGE) 46 | target_compile_options(bkcrack-core PUBLIC --coverage) 47 | target_link_options(bkcrack-core PUBLIC --coverage) 48 | 49 | find_program(BKCRACK_GCOVR_EXECUTABLE gcovr DOC "Path to gcovr program used to generate code coverage report." REQUIRED) 50 | 51 | add_custom_target(coverage 52 | COMMAND ${CMAKE_COMMAND} -E make_directory coverage 53 | COMMAND ${BKCRACK_GCOVR_EXECUTABLE} --root ${CMAKE_SOURCE_DIR} --html-nested coverage/index.html 54 | COMMENT "Generating code coverage HTML report" 55 | VERBATIM) 56 | 57 | add_custom_target(coveralls 58 | COMMAND ${BKCRACK_GCOVR_EXECUTABLE} --root ${CMAKE_SOURCE_DIR} --coveralls coverage.json 59 | COMMENT "Generating coverage.json report for coveralls.io" 60 | VERBATIM) 61 | endif() 62 | 63 | # code formatting 64 | find_program(BKCRACK_CLANG_FORMAT_EXECUTABLE clang-format DOC "Path to clang-format program used to format C++ code.") 65 | if(BKCRACK_CLANG_FORMAT_EXECUTABLE) 66 | set(files_to_format "") 67 | foreach(folder IN ITEMS include src tests) 68 | file(GLOB_RECURSE folder_files "${folder}/*.hpp" "${folder}/*.cpp") 69 | list(APPEND files_to_format ${folder_files}) 70 | endforeach() 71 | add_custom_target(format 72 | COMMAND ${BKCRACK_CLANG_FORMAT_EXECUTABLE} -i ${files_to_format} 73 | COMMENT "Formatting C++ code with ${BKCRACK_CLANG_FORMAT_EXECUTABLE}" 74 | VERBATIM) 75 | endif() 76 | 77 | # install rules 78 | install(DIRECTORY example DESTINATION .) 79 | install(DIRECTORY tools DESTINATION .) 80 | install(FILES readme.md DESTINATION .) 81 | install(FILES license.txt DESTINATION .) 82 | 83 | # package generation 84 | if(WIN32) 85 | set(CPACK_GENERATOR "ZIP") 86 | else() 87 | set(CPACK_GENERATOR "TGZ") 88 | endif() 89 | if(APPLE) 90 | set(CPACK_SYSTEM_NAME "macOS-${CMAKE_HOST_SYSTEM_PROCESSOR}") 91 | elseif(NOT WIN32) 92 | set(CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}") 93 | endif() 94 | include(CPack) 95 | -------------------------------------------------------------------------------- /tests/bkcrack/Zreduction.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | auto makeKeystream(std::size_t length) -> std::vector 12 | { 13 | auto keystream = std::vector{}; 14 | auto keys = Keys{"password"}; 15 | for (auto i = 0u; i < length; ++i) 16 | { 17 | keystream.emplace_back(keys.getK()); 18 | keys.update('A'); 19 | } 20 | return keystream; 21 | } 22 | 23 | auto getZvalue(std::size_t index) -> std::uint32_t 24 | { 25 | auto keys = Keys{"password"}; 26 | for (auto i = 0u; i < index; ++i) 27 | keys.update('A'); 28 | return keys.getZ(); 29 | } 30 | 31 | auto contains(const std::vector& vector, std::uint32_t value) -> bool 32 | { 33 | return std::find(vector.begin(), vector.end(), value) != vector.end(); 34 | } 35 | } // namespace 36 | 37 | TEST("generate only") 38 | { 39 | const auto keystream = makeKeystream(8); 40 | const auto z7 = getZvalue(7); 41 | 42 | auto zreduction = Zreduction{keystream}; 43 | CHECK(zreduction.getCandidates().size() == 2'752'512); 44 | CHECK(zreduction.getIndex() == 7); 45 | CHECK(contains(zreduction.getCandidates(), z7 & mask<10, 32>)); 46 | 47 | zreduction.generate(); 48 | CHECK(zreduction.getCandidates().size() == 4'194'304); 49 | CHECK(zreduction.getIndex() == 7); 50 | CHECK(contains(zreduction.getCandidates(), z7 & mask<2, 32>)); 51 | } 52 | 53 | TEST("reduce completely and generate") 54 | { 55 | const auto keystream = makeKeystream(12); 56 | const auto z7 = getZvalue(7); 57 | const auto z11 = getZvalue(11); 58 | 59 | auto zreduction = Zreduction{keystream}; 60 | CHECK(zreduction.getCandidates().size() == 2'686'976); 61 | CHECK(zreduction.getIndex() == 11); 62 | CHECK(contains(zreduction.getCandidates(), z11 & mask<10, 32>)); 63 | 64 | auto os = std::ostringstream{}; 65 | auto progress = Progress{os}; 66 | zreduction.reduce(progress); 67 | CHECK(progress.done == 4); 68 | CHECK(progress.total == 4); 69 | CHECK(progress.state == Progress::State::Normal); 70 | CHECK(os.str().empty()); 71 | CHECK(zreduction.getCandidates().size() == 898'165); 72 | CHECK(zreduction.getIndex() == 7); 73 | CHECK(contains(zreduction.getCandidates(), z7 & mask<10, 32>)); 74 | 75 | zreduction.generate(); 76 | CHECK(zreduction.getCandidates().size() == 1'368'208); 77 | CHECK(zreduction.getIndex() == 7); 78 | CHECK(contains(zreduction.getCandidates(), z7 & mask<2, 32>)); 79 | } 80 | 81 | TEST("reduce partially and generate") 82 | { 83 | const auto keystream = makeKeystream(14'336); 84 | const auto z9999 = getZvalue(9999); 85 | const auto z14335 = getZvalue(14335); 86 | 87 | auto zreduction = Zreduction{keystream}; 88 | CHECK(zreduction.getCandidates().size() == 2'686'976); 89 | CHECK(zreduction.getIndex() == 14335); 90 | CHECK(contains(zreduction.getCandidates(), z14335 & mask<10, 32>)); 91 | 92 | auto os = std::ostringstream{}; 93 | auto progress = Progress{os}; 94 | zreduction.reduce(progress); 95 | CHECK(progress.done == 5207); 96 | CHECK(progress.total == 14328); 97 | CHECK(progress.state == Progress::State::EarlyExit); 98 | CHECK(os.str().empty()); 99 | CHECK(zreduction.getCandidates().size() == 153); 100 | CHECK(zreduction.getIndex() == 9999); 101 | CHECK(contains(zreduction.getCandidates(), z9999 & mask<10, 32>)); 102 | 103 | zreduction.generate(); 104 | CHECK(zreduction.getCandidates().size() == 218); 105 | CHECK(zreduction.getIndex() == 9999); 106 | CHECK(contains(zreduction.getCandidates(), z9999 & mask<2, 32>)); 107 | } 108 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: ${{ matrix.platform.name }} 8 | runs-on: ${{ matrix.platform.os }} 9 | 10 | strategy: 11 | matrix: 12 | platform: 13 | - { name: Ubuntu x64, os: ubuntu-24.04 } 14 | - { name: Ubuntu arm64, os: ubuntu-24.04-arm } 15 | - { 16 | name: macOS x64, 17 | os: macos-latest, 18 | cmake-config: -DCMAKE_APPLE_SILICON_PROCESSOR=x86_64, 19 | } 20 | - { 21 | name: macOS arm64, 22 | os: macos-latest, 23 | cmake-config: -DCMAKE_APPLE_SILICON_PROCESSOR=arm64, 24 | } 25 | - { name: Windows 64-bit, os: windows-latest, cmake-config: -A x64 } 26 | - { name: Windows 32-bit, os: windows-latest, cmake-config: -A Win32 } 27 | 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | 32 | - name: Configure project 33 | run: cmake -S . -B build -DCMAKE_COMPILE_WARNING_AS_ERROR=ON -DCMAKE_LINK_WARNING_AS_ERROR=ON -DBKCRACK_BUILD_TESTING=ON ${{ matrix.platform.cmake-config }} 34 | 35 | - name: Build project 36 | run: cmake --build build --config Release 37 | 38 | - name: Run tests 39 | run: ctest --test-dir build -C Release --output-on-failure 40 | 41 | - name: Create package 42 | run: cmake --build build --config Release --target package 43 | 44 | - name: Upload package 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: ${{ matrix.platform.name }} package 48 | path: | 49 | build/*.zip 50 | build/*.tar.gz 51 | 52 | coverage: 53 | name: Code coverage 54 | runs-on: ubuntu-latest 55 | needs: [build, format] 56 | 57 | steps: 58 | - name: Install gcovr 59 | run: sudo apt update && sudo apt install gcovr 60 | 61 | - name: Checkout code 62 | uses: actions/checkout@v4 63 | 64 | - name: Configure project 65 | run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBKCRACK_BUILD_TESTING=ON -DBKCRACK_BUILD_COVERAGE=ON 66 | 67 | - name: Build project 68 | run: cmake --build build 69 | 70 | - name: Run tests 71 | run: cmake --build build --target test 72 | 73 | - name: Collect coverage report 74 | run: cmake --build build --target coveralls 75 | env: 76 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 77 | 78 | - name: Upload coverage report 79 | run: curl -X POST https://coveralls.io/api/v1/jobs -F "json_file=@build/coverage.json" --fail-with-body 80 | 81 | format: 82 | name: Formatting 83 | runs-on: ubuntu-latest 84 | 85 | steps: 86 | - name: Checkout code 87 | uses: actions/checkout@v4 88 | 89 | - name: Configure project 90 | run: cmake -S . -B build -DBKCRACK_CLANG_FORMAT_EXECUTABLE=/usr/bin/clang-format-17 91 | 92 | - name: Format C++ code 93 | run: cmake --build build --target format 94 | 95 | - name: Check for difference 96 | run: git diff --exit-code 97 | 98 | release: 99 | name: Release 100 | runs-on: ubuntu-latest 101 | needs: [build, coverage, format] 102 | if: startsWith(github.ref, 'refs/tags/') 103 | 104 | steps: 105 | - name: Download packages 106 | uses: actions/download-artifact@v4 107 | with: 108 | pattern: "* package" 109 | path: packages 110 | merge-multiple: true 111 | 112 | - name: Create release 113 | uses: softprops/action-gh-release@v2 114 | with: 115 | name: Release ${{ github.ref_name }} 116 | files: packages/* 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | -------------------------------------------------------------------------------- /tests/runner/TestRunner.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_TESTRUNNER_HPP 2 | #define BKCRACK_TESTRUNNER_HPP 3 | 4 | struct TestError 5 | { 6 | const char* expression; 7 | }; 8 | 9 | #define CHECK(...) \ 10 | do \ 11 | { \ 12 | if (!(__VA_ARGS__)) \ 13 | throw TestError{#__VA_ARGS__}; \ 14 | } while (false) 15 | 16 | auto checkMessageContains(const char* actual, const char* expected) -> bool; 17 | 18 | #define CHECK_THROWS(ErrorType, message, ...) \ 19 | do \ 20 | { \ 21 | try \ 22 | { \ 23 | (void)__VA_ARGS__; \ 24 | } \ 25 | catch (const ErrorType& error) \ 26 | { \ 27 | if (checkMessageContains(error.what(), message)) \ 28 | break; \ 29 | else \ 30 | throw TestError{#__VA_ARGS__ " should throw " #ErrorType " with message \"" message "\""}; \ 31 | } \ 32 | throw TestError{#__VA_ARGS__ " should throw " #ErrorType}; \ 33 | } while (false) 34 | 35 | struct TestRegistration 36 | { 37 | TestRegistration(const char* name, void (&function)()); 38 | }; 39 | 40 | #define CONCAT_IMPL(prefix, line) prefix##line 41 | #define CONCAT(prefix, line) CONCAT_IMPL(prefix, line) 42 | #define IDENTIFIER_WITH_LINE(prefix) CONCAT(prefix, __LINE__) 43 | 44 | #define TEST(name) \ 45 | void IDENTIFIER_WITH_LINE(testFunction)(); \ 46 | namespace \ 47 | { \ 48 | const auto IDENTIFIER_WITH_LINE(testRegistration) = TestRegistration{name, IDENTIFIER_WITH_LINE(testFunction)}; \ 49 | } \ 50 | void IDENTIFIER_WITH_LINE(testFunction)() 51 | 52 | struct TestRunner 53 | { 54 | static auto runAllTests() -> bool; 55 | }; 56 | 57 | #endif // BKCRACK_TESTRUNNER_HPP 58 | -------------------------------------------------------------------------------- /tests/bkcrack/Attack.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace 8 | { 9 | const auto plaintext = std::vector{ 10 | 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', 11 | }; 12 | const auto ciphertext = std::vector{ 13 | 0x3e, 0xb4, 0xc5, 0x92, 0x58, 0x40, 0x9a, 0x6c, 0xed, 0x99, 0x65, 0x81, 14 | 0x66, 0x1b, 0x1d, 0xda, 0x5d, 0x8a, 0x8c, 0x30, 0x07, 0x76, 0x50, 0xbb, 15 | }; 16 | 17 | const auto z7 = 0x74930e66; 18 | const auto z7_2_32 = std::vector{ 19 | 0x00000000, 0x10000000, 0x20000000, 0x30000000, 0x40000000, 0x50000000, 0x60000000, z7& mask<2, 32>, 20 | 0x80000000, 0x90000000, 0xa0000000, 0xb0000000, 0xc0000000, 0xd0000000, 0xe0000000, 0xf0000000, 21 | }; 22 | 23 | const auto z9 = 0x69196cee; 24 | const auto z9_2_32 = std::vector{ 25 | 0x00000000, 0x10000000, 0x20000000, 0x30000000, 0x40000000, 0x50000000, z9& mask<2, 32>, 0x70000000, 26 | 0x80000000, 0x90000000, 0xa0000000, 0xb0000000, 0xc0000000, 0xd0000000, 0xe0000000, 0xf0000000, 27 | }; 28 | } // namespace 29 | 30 | TEST("simple case") 31 | { 32 | const auto data = Data{ciphertext, plaintext, 0, {}}; 33 | auto start = 0; 34 | auto os = std::ostringstream{}; 35 | auto progress = Progress{os}; 36 | 37 | const auto result = attack(data, z7_2_32, start, 7, 1, false, progress); 38 | 39 | CHECK(result.size() == 1); 40 | CHECK(result[0].getX() == 0xea9b4e4d); 41 | CHECK(result[0].getY() == 0xba789085); 42 | CHECK(result[0].getZ() == 0x5ff8707d); 43 | 44 | CHECK(start == 8); 45 | CHECK(progress.done == 8); 46 | CHECK(progress.total == 16); 47 | CHECK(progress.state == Progress::State::EarlyExit); 48 | } 49 | 50 | TEST("offset and extra plaintext") 51 | { 52 | const auto plaintext2 = std::vector{plaintext.begin() + 2, plaintext.end() - 2}; 53 | const auto extra = std::map{{-2, 0x65 ^ 0x7d}, {-1, 0x81 ^ 0x0d}, {0, 'H'}, {11, '!'}}; 54 | const auto data = Data{ciphertext, plaintext2, 2, extra}; 55 | auto start = 0; 56 | auto os = std::ostringstream{}; 57 | auto progress = Progress{os}; 58 | 59 | const auto result = attack(data, z9_2_32, start, 7, 1, false, progress); 60 | 61 | CHECK(result.size() == 1); 62 | CHECK(result[0].getX() == 0xea9b4e4d); 63 | CHECK(result[0].getY() == 0xba789085); 64 | CHECK(result[0].getZ() == 0x5ff8707d); 65 | 66 | CHECK(start == 7); 67 | CHECK(progress.done == 7); 68 | CHECK(progress.total == 16); 69 | CHECK(progress.state == Progress::State::EarlyExit); 70 | } 71 | 72 | TEST("restart point before solution") 73 | { 74 | const auto data = Data{ciphertext, plaintext, 0, {}}; 75 | auto start = 4; 76 | auto os = std::ostringstream{}; 77 | auto progress = Progress{os}; 78 | 79 | const auto result = attack(data, z7_2_32, start, 7, 1, false, progress); 80 | 81 | CHECK(result.size() == 1); 82 | CHECK(result[0].getX() == 0xea9b4e4d); 83 | CHECK(result[0].getY() == 0xba789085); 84 | CHECK(result[0].getZ() == 0x5ff8707d); 85 | 86 | CHECK(start == 8); 87 | CHECK(progress.done == 8); 88 | CHECK(progress.total == 16); 89 | CHECK(progress.state == Progress::State::EarlyExit); 90 | } 91 | 92 | TEST("restart point past solution") 93 | { 94 | const auto data = Data{ciphertext, plaintext, 0, {}}; 95 | auto start = 8; 96 | auto os = std::ostringstream{}; 97 | auto progress = Progress{os}; 98 | 99 | const auto result = attack(data, z7_2_32, start, 7, 1, false, progress); 100 | 101 | CHECK(result.empty()); 102 | 103 | CHECK(start == 16); 104 | CHECK(progress.done == 16); 105 | CHECK(progress.total == 16); 106 | CHECK(progress.state == Progress::State::Normal); 107 | } 108 | 109 | TEST("exhaustive attack") 110 | { 111 | const auto data = Data{ciphertext, plaintext, 0, {}}; 112 | auto start = 0; 113 | auto os = std::ostringstream{}; 114 | auto progress = Progress{os}; 115 | 116 | const auto result = attack(data, z7_2_32, start, 7, 1, true, progress); 117 | 118 | CHECK(result.size() == 1); 119 | CHECK(result[0].getX() == 0xea9b4e4d); 120 | CHECK(result[0].getY() == 0xba789085); 121 | CHECK(result[0].getZ() == 0x5ff8707d); 122 | 123 | CHECK(start == 16); 124 | CHECK(progress.done == 16); 125 | CHECK(progress.total == 16); 126 | CHECK(progress.state == Progress::State::Normal); 127 | } 128 | -------------------------------------------------------------------------------- /src/bkcrack/Zreduction.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/Zreduction.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | Zreduction::Zreduction(const std::vector& keystream) 11 | : keystream{keystream} 12 | { 13 | index = keystream.size() - 1; 14 | zi_vector.reserve(1 << 22); 15 | 16 | for (auto zi_10_32_shifted = std::uint32_t{}; zi_10_32_shifted < 1 << 22; zi_10_32_shifted++) 17 | if (KeystreamTab::hasZi_2_16(keystream[index], zi_10_32_shifted << 10)) 18 | zi_vector.push_back(zi_10_32_shifted << 10); 19 | } 20 | 21 | void Zreduction::reduce(Progress& progress) 22 | { 23 | // variables to keep track of the smallest Zi[2,32) vector 24 | constexpr auto trackSizeThreshold = std::size_t{1 << 16}; 25 | 26 | auto tracking = false; 27 | auto bestCopy = std::vector{}; 28 | auto bestIndex = index; 29 | auto bestSize = trackSizeThreshold; 30 | 31 | // variables to wait for a limited number of steps when a small enough vector is found 32 | constexpr auto waitSizeThreshold = std::size_t{1 << 8}; 33 | 34 | auto waiting = false; 35 | auto wait = std::size_t{}; 36 | 37 | auto zim1_10_32_vector = std::vector{}; 38 | zim1_10_32_vector.reserve(1 << 22); 39 | auto zim1_10_32_set = std::bitset<1 << 22>{}; 40 | 41 | progress.done = 0; 42 | progress.total = keystream.size() - Attack::contiguousSize; 43 | 44 | for (auto i = index; i >= Attack::contiguousSize; i--) 45 | { 46 | zim1_10_32_vector.clear(); 47 | zim1_10_32_set.reset(); 48 | auto number_of_zim1_2_32 = std::size_t{}; 49 | 50 | // generate the Z{i-1}[10,32) values 51 | for (const auto zi_10_32 : zi_vector) 52 | for (const auto zi_2_16 : KeystreamTab::getZi_2_16_vector(keystream[i], zi_10_32)) 53 | { 54 | // get Z{i-1}[10,32) from CRC32^-1 55 | const auto zim1_10_32 = Crc32Tab::getZim1_10_32(zi_10_32 | zi_2_16); 56 | // collect without duplicates only those that are compatible with keystream{i-1} 57 | if (!zim1_10_32_set[zim1_10_32 >> 10] && KeystreamTab::hasZi_2_16(keystream[i - 1], zim1_10_32)) 58 | { 59 | zim1_10_32_vector.push_back(zim1_10_32); 60 | zim1_10_32_set.set(zim1_10_32 >> 10); 61 | number_of_zim1_2_32 += KeystreamTab::getZi_2_16_vector(keystream[i - 1], zim1_10_32).size(); 62 | } 63 | } 64 | 65 | // update smallest vector tracking 66 | if (number_of_zim1_2_32 <= bestSize) // new smallest number of Z[2,32) values 67 | { 68 | tracking = true; 69 | bestIndex = i - 1; 70 | bestSize = number_of_zim1_2_32; 71 | waiting = false; 72 | } 73 | else if (tracking) // number of Z{i-1}[2,32) values is bigger than bestSize 74 | { 75 | if (bestIndex == i) // hit a minimum 76 | { 77 | // keep a copy of the vector because size is about to grow 78 | std::swap(bestCopy, zi_vector); 79 | 80 | if (bestSize <= waitSizeThreshold) 81 | { 82 | // enable waiting 83 | waiting = true; 84 | wait = bestSize * 4; // arbitrary multiplicative constant 85 | } 86 | } 87 | 88 | if (waiting && --wait == 0) 89 | { 90 | progress.state = Progress::State::EarlyExit; 91 | break; 92 | } 93 | } 94 | 95 | // put result in zi_vector 96 | std::swap(zi_vector, zim1_10_32_vector); 97 | 98 | progress.done++; 99 | } 100 | 101 | if (tracking) 102 | { 103 | // put bestCopy in zi_vector only if bestIndex is not the index of zi_vector 104 | if (bestIndex != Attack::contiguousSize - 1) 105 | std::swap(zi_vector, bestCopy); 106 | index = bestIndex; 107 | } 108 | else 109 | index = Attack::contiguousSize - 1; 110 | } 111 | 112 | void Zreduction::generate() 113 | { 114 | const auto number_of_zi_10_32 = zi_vector.size(); 115 | for (auto i = std::size_t{}; i < number_of_zi_10_32; i++) 116 | { 117 | const auto& zi_2_16_vector = KeystreamTab::getZi_2_16_vector(keystream[index], zi_vector[i]); 118 | for (auto j = std::size_t{1}; j < zi_2_16_vector.size(); j++) 119 | zi_vector.push_back(zi_vector[i] | zi_2_16_vector[j]); 120 | zi_vector[i] |= zi_2_16_vector[0]; 121 | } 122 | } 123 | 124 | auto Zreduction::getCandidates() const -> const std::vector& 125 | { 126 | return zi_vector; 127 | } 128 | 129 | auto Zreduction::getIndex() const -> std::size_t 130 | { 131 | return index; 132 | } 133 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(runner) 2 | add_subdirectory(bkcrack) 3 | add_subdirectory(cli) 4 | 5 | add_test(NAME cli.attack 6 | COMMAND bkcrack -C secrets.zip -c spiral.svg -x 0 3c3f786d6c2076657273696f6e3d22312e302220 --continue-attack 183500 7 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/example) 8 | set_tests_properties(cli.attack PROPERTIES PASS_REGULAR_EXPRESSION " 9 | .* Z reduction using 13 bytes of known plaintext 10 | .* Attack on 542303 Z values at index 6 11 | .* Keys 12 | c4490e28 b414a23d 91404b31 13 | ") 14 | 15 | add_test(NAME cli.password COMMAND bkcrack --password W4sF0rgotten) 16 | set_tests_properties(cli.password PROPERTIES PASS_REGULAR_EXPRESSION "c4490e28 b414a23d 91404b31") 17 | 18 | add_test(NAME cli.decipher COMMAND ${CMAKE_COMMAND} 19 | -DBKCRACK_COMMAND=$ 20 | -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} 21 | -P ${CMAKE_CURRENT_SOURCE_DIR}/decipher.cmake) 22 | 23 | add_test(NAME cli.decrypt COMMAND ${CMAKE_COMMAND} 24 | -DBKCRACK_COMMAND=$ 25 | -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} 26 | -P ${CMAKE_CURRENT_SOURCE_DIR}/decrypt.cmake) 27 | 28 | add_test(NAME cli.change-password COMMAND ${CMAKE_COMMAND} 29 | -DBKCRACK_COMMAND=$ 30 | -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} 31 | -P ${CMAKE_CURRENT_SOURCE_DIR}/change-password.cmake) 32 | 33 | add_test(NAME cli.change-keys COMMAND ${CMAKE_COMMAND} 34 | -DBKCRACK_COMMAND=$ 35 | -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} 36 | -P ${CMAKE_CURRENT_SOURCE_DIR}/change-keys.cmake) 37 | 38 | add_test(NAME cli.bruteforce.empty COMMAND bkcrack -k 12345678 23456789 34567890 -r 0 ?b) 39 | add_test(NAME cli.bruteforce.empty-small-charset COMMAND bkcrack -k 12345678 23456789 34567890 -r 0 a) 40 | add_test(NAME cli.bruteforce.tiny COMMAND bkcrack -k 1b226dfe c089e0a3 6af00ee6 -r 4 ?b) 41 | add_test(NAME cli.bruteforce.small COMMAND bkcrack -k 9bcb20c6 10a97ca5 103c0614 -r 8 ?p) 42 | add_test(NAME cli.bruteforce.medium COMMAND bkcrack -k edb43a00 9ce6e179 8cf2cbba -r 10 ?a) 43 | add_test(NAME cli.bruteforce.long COMMAND bkcrack -k dcce7593 b8a2e617 b2bd4365 -r 12 ?l) 44 | set_tests_properties(cli.bruteforce.empty PROPERTIES PASS_REGULAR_EXPRESSION "Password: \n") 45 | set_tests_properties(cli.bruteforce.empty-small-charset PROPERTIES PASS_REGULAR_EXPRESSION "Password: \n") 46 | set_tests_properties(cli.bruteforce.tiny PROPERTIES PASS_REGULAR_EXPRESSION "Password: 🔐\n.*Password\nas bytes: f0 9f 94 90\nas text: 🔐\n") 47 | set_tests_properties(cli.bruteforce.small PROPERTIES PASS_REGULAR_EXPRESSION "Password: _S#cr3t!\n") 48 | set_tests_properties(cli.bruteforce.medium PROPERTIES PASS_REGULAR_EXPRESSION "Password: q1w2e3r4t5\n") 49 | set_tests_properties(cli.bruteforce.long PROPERTIES PASS_REGULAR_EXPRESSION "Password: abcdefghijkl\n") 50 | 51 | add_test(NAME cli.mask.empty COMMAND bkcrack -k 12345678 23456789 34567890 -m "") 52 | add_test(NAME cli.mask.five COMMAND bkcrack -k 5e07e483 0c4900a4 4e586ac1 -m ?u?l?d?s.) 53 | add_test(NAME cli.mask.six COMMAND bkcrack -k f9720e40 2520f2b9 0a5660df -m ?d?l?d?l?d?l) 54 | add_test(NAME cli.mask.seven COMMAND bkcrack -k 2af9b027 85bd8154 286ca64f -m ?l?l?l?l?l?l?l) 55 | add_test(NAME cli.mask.long COMMAND bkcrack -k 0d892b8b 02dd8fad 77f52c7b -m ?l?l?l?l?l?l?l?l-?d?d?d?d) 56 | set_tests_properties(cli.mask.empty PROPERTIES PASS_REGULAR_EXPRESSION "Password: \n") 57 | set_tests_properties(cli.mask.five PROPERTIES PASS_REGULAR_EXPRESSION "Password: Aa1_.\n") 58 | set_tests_properties(cli.mask.six PROPERTIES PASS_REGULAR_EXPRESSION "Password: 1q2w3e\n") 59 | set_tests_properties(cli.mask.seven PROPERTIES PASS_REGULAR_EXPRESSION "Password: letmein\n") 60 | set_tests_properties(cli.mask.long PROPERTIES PASS_REGULAR_EXPRESSION "Password: password-1234\n") 61 | 62 | add_test(NAME cli.mask.constant COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -m "Lorem ipsum dolor sit amet") 63 | add_test(NAME cli.mask.suffix COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -m "Lorem ipsum dolor ?1?1?1?1?1?1?1?1" -s 1 ?s?l) 64 | add_test(NAME cli.mask.prefix COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -m "?u?1?1?1?1 ipsum dolor sit amet" -s 1 ?s?l) 65 | add_test(NAME cli.mask.factor COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -m "Lorem ipsum ?1?1?1?1?1?1?1?1?1 amet" -s 1 ?s?l) 66 | add_test(NAME cli.mask.sparse COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -m "Lo?le?l i?ls?lm do?lo?l sit a?let") 67 | set_tests_properties(cli.mask.constant PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") 68 | set_tests_properties(cli.mask.suffix PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") 69 | set_tests_properties(cli.mask.prefix PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") 70 | set_tests_properties(cli.mask.factor PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") 71 | set_tests_properties(cli.mask.sparse PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") 72 | 73 | add_test(NAME cli.charset.one-letter COMMAND bkcrack --password aaaaaa -r 6 ?0 -s 0 a) 74 | add_test(NAME cli.charset.four-letters COMMAND bkcrack --password abcabc -r 6 ?0 -s 0 abcd) 75 | add_test(NAME cli.charset.question-mark COMMAND bkcrack --password hello? -r 6 ?0 -s 0 ?l?) 76 | add_test(NAME cli.charset.hexadecimal COMMAND bkcrack --password 1a2b3c -r 6 ?h -s h ?dabcdef) 77 | 78 | add_test(NAME cli.charset.short-cycle COMMAND bkcrack --password "" -r 0 ?0 -s 0 ?0) 79 | add_test(NAME cli.charset.long-cycle COMMAND bkcrack --password "" -r 0 ?0 -s 0 ?1 -s 1 ?2 -s 2 ?3 -s 3 ?4 -s 4 ?5 -s 5 ?0) 80 | set_tests_properties(cli.charset.short-cycle PROPERTIES PASS_REGULAR_EXPRESSION "circular reference") 81 | set_tests_properties(cli.charset.long-cycle PROPERTIES PASS_REGULAR_EXPRESSION "circular reference") 82 | 83 | add_test(NAME cli.list COMMAND bkcrack -L secrets.zip WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/example) 84 | set_tests_properties(cli.list PROPERTIES PASS_REGULAR_EXPRESSION " 85 | Index Encryption Compression CRC32 Uncompressed Packed size Name 86 | ----- ---------- ----------- -------- ------------ ------------ ---------------- 87 | 0 ZipCrypto Deflate 7ca9f10a 54799 54700 advice.jpg 88 | 1 ZipCrypto Store a99f1d0d 1265 1277 spiral.svg 89 | ") 90 | 91 | add_test(NAME cli.version COMMAND bkcrack --version) 92 | set_tests_properties(cli.version PROPERTIES PASS_REGULAR_EXPRESSION "^bkcrack ${bkcrack_VERSION} - ${bkcrack_VERSION_DATE}\n\$") 93 | 94 | add_test(NAME cli.help COMMAND bkcrack -h) 95 | set_tests_properties(cli.help PROPERTIES PASS_REGULAR_EXPRESSION "usage:") 96 | -------------------------------------------------------------------------------- /tests/bkcrack/Data.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace 9 | { 10 | const auto plaintext = std::vector{ 11 | 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', 12 | }; 13 | const auto ciphertext = std::vector{ 14 | 0x3e, 0xb4, 0xc5, 0x92, 0x58, 0x40, 0x9a, 0x6c, 0xed, 0x99, 0x65, 0x81, 15 | 0x66, 0x1b, 0x1d, 0xda, 0x5d, 0x8a, 0x8c, 0x30, 0x07, 0x76, 0x50, 0xbb, 16 | }; 17 | const auto keystream = std::vector{ 18 | 0xee, 0x96, 0x22, 0x47, 0x78, 0xb8, 0x73, 0x54, 0x4c, 0xd7, 0x7d, 0x0d, 19 | 0x2e, 0x7e, 0x71, 0xb6, 0x32, 0xaa, 0xdb, 0x5f, 0x75, 0x1a, 0x34, 0x9a, 20 | }; 21 | 22 | auto prefix(const std::vector& vector, std::size_t length) -> std::vector 23 | { 24 | return std::vector{vector.begin(), vector.begin() + length}; 25 | } 26 | auto slice(const std::vector& vector, std::size_t begin, std::size_t end) -> std::vector 27 | { 28 | return std::vector{vector.begin() + begin, vector.begin() + end}; 29 | } 30 | } // namespace 31 | 32 | TEST("Data::Error") 33 | { 34 | const auto error = Data::Error{"description"}; 35 | CHECK(error.what() == std::string_view{"Data error: description."}); 36 | } 37 | 38 | TEST("contiguous plaintext only") 39 | { 40 | const auto data = Data{ciphertext, plaintext, 0, {}}; 41 | CHECK(data.ciphertext == ciphertext); 42 | CHECK(data.plaintext == plaintext); 43 | CHECK(data.offset == 12); 44 | CHECK(data.keystream == slice(keystream, 12, 24)); 45 | CHECK(data.extraPlaintext.empty()); 46 | } 47 | 48 | TEST("contiguous plaintext and sparse extra plaintext") 49 | { 50 | const auto data = Data{ciphertext, prefix(plaintext, 8), 0, {{-3, 0x4e}, {-2, 0x18}, {10, 'd'}, {11, '!'}}}; 51 | CHECK(data.ciphertext == ciphertext); 52 | CHECK(data.plaintext == prefix(plaintext, 8)); 53 | CHECK(data.offset == 12); 54 | CHECK(data.keystream == slice(keystream, 12, 20)); 55 | CHECK(data.extraPlaintext == decltype(data.extraPlaintext){{22, 'd'}, {23, '!'}, {10, 0x18}, {9, 0x4e}}); 56 | } 57 | 58 | TEST("merge extra plaintext after") 59 | { 60 | const auto data = Data{ciphertext, prefix(plaintext, 10), 0, {{10, 'd'}, {11, '!'}}}; 61 | CHECK(data.ciphertext == ciphertext); 62 | CHECK(data.plaintext == plaintext); 63 | CHECK(data.offset == 12); 64 | CHECK(data.keystream == slice(keystream, 12, 24)); 65 | CHECK(data.extraPlaintext.empty()); 66 | } 67 | 68 | TEST("merge extra plaintext before") 69 | { 70 | const auto data = Data{ciphertext, slice(plaintext, 2, 12), 2, {{0, 'H'}, {1, 'e'}}}; 71 | CHECK(data.ciphertext == ciphertext); 72 | CHECK(data.plaintext == plaintext); 73 | CHECK(data.offset == 12); 74 | CHECK(data.keystream == slice(keystream, 12, 24)); 75 | CHECK(data.extraPlaintext.empty()); 76 | } 77 | 78 | TEST("overwrite contiguous plaintext with extra plaintext") 79 | { 80 | auto overwrittenPlaintext = plaintext; 81 | overwrittenPlaintext[5] = '*'; 82 | 83 | auto overwrittenKeystream = keystream; 84 | overwrittenKeystream[17] = ciphertext[17] ^ '*'; 85 | 86 | const auto data = Data{ciphertext, plaintext, 0, {{5, '*'}}}; 87 | CHECK(data.ciphertext == ciphertext); 88 | CHECK(data.plaintext == overwrittenPlaintext); 89 | CHECK(data.offset == 12); 90 | CHECK(data.keystream == slice(overwrittenKeystream, 12, 24)); 91 | CHECK(data.extraPlaintext.empty()); 92 | } 93 | 94 | TEST("long contiguous extra plaintext after") 95 | { 96 | const auto data = Data{ciphertext, 97 | {0x8c, 'H', 'e', 'l'}, 98 | -1, 99 | {{4, 'o'}, {5, ' '}, {6, 'W'}, {7, 'o'}, {8, 'r'}, {9, 'l'}, {10, 'd'}, {11, '!'}}}; 100 | CHECK(data.ciphertext == ciphertext); 101 | CHECK(data.plaintext == slice(plaintext, 4, 12)); 102 | CHECK(data.offset == 16); 103 | CHECK(data.keystream == slice(keystream, 16, 24)); 104 | CHECK(data.extraPlaintext == decltype(data.extraPlaintext){{14, 'l'}, {13, 'e'}, {12, 'H'}, {11, 0x8c}}); 105 | } 106 | 107 | TEST("long contiguous extra plaintext before") 108 | { 109 | const auto data = Data{ciphertext, 110 | {'r', 'l', 'd', '!'}, 111 | 8, 112 | {{-1, 0x8c}, {0, 'H'}, {1, 'e'}, {2, 'l'}, {3, 'l'}, {4, 'o'}, {5, ' '}, {6, 'W'}}}; 113 | CHECK(data.ciphertext == ciphertext); 114 | CHECK(data.plaintext == decltype(data.plaintext){0x8c, 'H', 'e', 'l', 'l', 'o', ' ', 'W'}); 115 | CHECK(data.offset == 11); 116 | CHECK(data.keystream == slice(keystream, 11, 19)); 117 | CHECK(data.extraPlaintext == decltype(data.extraPlaintext){{20, 'r'}, {21, 'l'}, {22, 'd'}, {23, '!'}}); 118 | } 119 | 120 | TEST("extra plaintext only") 121 | { 122 | const auto extraPlaintext = std::map{ 123 | {0, 'H'}, {1, 'e'}, {2, 'l'}, {3, 'l'}, {4, 'o'}, {5, ' '}, 124 | {6, 'W'}, {7, 'o'}, {8, 'r'}, {9, 'l'}, {10, 'd'}, {11, '!'}, 125 | }; 126 | const auto data = Data{ciphertext, {}, -1, extraPlaintext}; 127 | CHECK(data.ciphertext == ciphertext); 128 | CHECK(data.plaintext == plaintext); 129 | CHECK(data.offset == 12); 130 | CHECK(data.keystream == slice(keystream, 12, 24)); 131 | CHECK(data.extraPlaintext.empty()); 132 | } 133 | 134 | TEST("not enough data") 135 | { 136 | CHECK_THROWS(Data::Error, "ciphertext is too small for an attack (minimum length is 12)", 137 | Data{prefix(ciphertext, 11), prefix(plaintext, 11), -12, {}}); 138 | 139 | CHECK_THROWS(Data::Error, "ciphertext is smaller than plaintext", 140 | Data{prefix(ciphertext, 12), std::vector('A', 13), -12, {}}); 141 | 142 | CHECK_THROWS(Data::Error, "not enough contiguous plaintext (7 bytes available, minimum is 8)", 143 | Data{ciphertext, prefix(plaintext, 7), 0, {}}); 144 | 145 | CHECK_THROWS(Data::Error, "not enough plaintext (11 bytes available, minimum is 12)", 146 | Data{ciphertext, prefix(plaintext, 9), 0, {{10, 'd'}, {11, '!'}}}); 147 | } 148 | 149 | TEST("invalid offset") 150 | { 151 | CHECK_THROWS(Data::Error, "plaintext offset -13 is too small (minimum is -12)", 152 | Data{ciphertext, plaintext, -13, {}}); 153 | 154 | CHECK_THROWS(Data::Error, "plaintext offset 1 is too large", Data{ciphertext, plaintext, 1, {}}); 155 | 156 | CHECK_THROWS(Data::Error, "extra plaintext offset -13 is too small (minimum is -12)", 157 | Data{ciphertext, plaintext, 0, {{-13, 0x00}}}); 158 | 159 | CHECK_THROWS(Data::Error, "extra plaintext offset 12 is too large", Data{ciphertext, plaintext, 0, {{12, 0x00}}}); 160 | } 161 | -------------------------------------------------------------------------------- /doc/resources.md: -------------------------------------------------------------------------------- 1 | Resources {#resources} 2 | ========= 3 | 4 | \brief Related publications, online resources and tools. 5 | 6 | \if not_doxygen 7 | 8 | **Some commands below are for doxygen only.** 9 | **They are not rendered when viewing the file on GitHub.** 10 | 11 | \endif 12 | 13 | # Research papers 14 | 15 | - \anchor BK94 [A known plaintext attack on the PKZIP stream cipher](https://link.springer.com/content/pdf/10.1007/3-540-60590-8_12.pdf) 16 | 17 | Biham E., Kocher P.C. (1995) A known plaintext attack on the PKZIP stream cipher. In: Preneel B. (eds) Fast Software Encryption. FSE 1994. Lecture Notes in Computer Science, vol 1008. Springer, Berlin, Heidelberg. 18 | [DOI](https://doi.org/10.1007/3-540-60590-8_12) 19 | 20 | Describes a known plaintext attack on the PKZIP stream cipher. 21 | Requires 13 bytes of known plaintext: 8 for generating 2^38 candidates and 5 for filtering candidates. 22 | 23 | There are several parts: 24 | + Optionally, using additional contiguous known plaintext to reduce the number of candidates. 25 | + Finding the password internal representation. 26 | + Recovering the password. 27 | 28 | bkcrack is based on this paper. 29 | 30 | - [ZIP Attacks with Reduced Known Plaintext](https://link.springer.com/content/pdf/10.1007/3-540-45473-X_10.pdf) 31 | 32 | Stay M. (2002) ZIP Attacks with Reduced Known Plaintext. In: Matsui M. (eds) Fast Software Encryption. FSE 2001. Lecture Notes in Computer Science, vol 2355. Springer, Berlin, Heidelberg. 33 | [DOI](https://doi.org/10.1007/3-540-45473-X_10) 34 | 35 | Reviews Biham and Kocher attack. 36 | Suggests a small improvement to require 12 bytes instead of 13 bytes (not throwing away 6 known bits in Y7). 37 | Suggests using CRC-32 check bytes from several files as known plaintext. 38 | 39 | Then, it presents other approaches. 40 | One is using 4 bytes of known plaintext to generate 2^63 candidates. 41 | The other uses a weakness in a random number generator. 42 | 43 | - An Improved Known Plaintext %Attack on PKZIP Encryption Algorithm 44 | 45 | Jeong K.C., Lee D.H., Han D. (2012) An Improved Known Plaintext %Attack on PKZIP Encryption Algorithm. In: Kim H. (eds) Information Security and Cryptology. ICISC 2011. Lecture Notes in Computer Science, vol 7259. Springer, Berlin, Heidelberg. 46 | [DOI](https://doi.org/10.1007/978-3-642-31912-9_16) 47 | 48 | About speeding up the attack using known plaintext from several files. 49 | It assumes the very first bytes are known. 50 | However, the very first encrypted bytes are from the encryption header which starts with 10 or 11 random bytes. 51 | So, it does not seem practical unless the pseudo-random number generator used to fill the encryption header is broken. 52 | 53 | - \anchor Coray2019 [Improved Forensic Recovery of PKZIP Stream Cipher Passwords](https://www.scitepress.org/Papers/2019/73605/73605.pdf) 54 | 55 | Coray, S., Coisel, I., Sanchez, I. (2019). Improved Forensic Recovery of PKZIP Stream Cipher Passwords. In Proceedings of the 5th International Conference on Information Systems Security and Privacy - Volume 1: ICISSP, ISBN 978-989-758-359-9, pages 328-335. 56 | [DOI](https://doi.org/10.5220/0007360503280335) 57 | 58 | About finding the actual password, either using the internal keys or not. Does computations on the GPU with OpenCL. 59 | 60 | Implemented in \ref hashcat : 61 | + %Attack on the password without plaintext: https://github.com/hashcat/hashcat/pull/1962 62 | + Recovering the password from the internal keys: https://github.com/hashcat/hashcat/pull/2032 63 | 64 | # Books 65 | 66 | - Applied Cryptanalysis: Breaking Ciphers in the Real World 67 | 68 | Stamp, M., & Low, R. M. (2007). Applied cryptanalysis: breaking ciphers in the real world. John Wiley & Sons. 69 | 70 | Contains a chapter about stream ciphers. 71 | A section is dedicated to PKZIP encryption and \ref BK94 "Biham and Kocher attack". 72 | 73 | + [Editor's page](https://www.wiley.com/en-us/-p-9780470148778) 74 | + [Author's page](http://www.cs.sjsu.edu/~stamp/crypto/) 75 | + [Author's slides on PKZIP attack](http://www.cs.sjsu.edu/~stamp/crypto/PowerPoint_PDF/8_PKZIP.pdf) 76 | 77 | # ZIP specification 78 | 79 | - \anchor APPNOTE [APPNOTE.TXT - .ZIP File Format Specification](https://www.pkware.com/documents/casestudies/APPNOTE.TXT) 80 | 81 | Published by PKWARE, Inc. which developed the ZIP format. 82 | 83 | - [RFC1951 - DEFLATE Compressed %Data Format Specification](http://www.ietf.org/rfc/rfc1951.txt) 84 | 85 | Deflate compression algorithm is often used in ZIP files. 86 | 87 | - [Microsoft Docs - DosDateTimeToFileTime function](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime) 88 | 89 | Microsoft documentation page describing the date and time format used in ZIP date and time fields. 90 | 91 | # Tools 92 | 93 | ## Cracking internal keys 94 | 95 | - [PkCrack](https://www.unix-ag.uni-kl.de/~conrad/krypto/pkcrack.html) 96 | 97 | Biham and Kocher attack implementation by Peter Conrad. 98 | 99 | License: Postcardware 100 | 101 | - [Aloxaf/rbkcrack](https://github.com/Aloxaf/rbkcrack) 102 | 103 | A Rust rewrite of bkcrack by Aloxaf. 104 | Added ZIP64 support long before bkcrack. 105 | 106 | License: zlib 107 | 108 | ## Password recovery 109 | 110 | - \anchor hashcat [hashcat](https://hashcat.net/) 111 | 112 | Password recovery tool. See \ref Coray2019. 113 | 114 | License: MIT 115 | 116 | - [John the Ripper](https://www.openwall.com/john/) 117 | 118 | Password recovery tool. 119 | 120 | License: GNU General Public License v2.0 (Almost, see [LICENSE](https://github.com/openwall/john/blob/bleeding-jumbo/doc/LICENSE)) 121 | 122 | - [mferland/libzc](https://github.com/mferland/libzc) 123 | 124 | Tool and library for cracking legacy zip files by Marc Ferland. 125 | Implements bruteforce, dictionary and known plaintext attacks to recover the password. 126 | 127 | License: GNU General Public License v3.0 128 | 129 | ## Other tools 130 | 131 | - [Aloxaf/p7zip](https://github.com/Aloxaf/p7zip) 132 | 133 | A patched p7zip by Aloxaf. 134 | Supports ZIP file extraction using the internal keys with the following syntax: 135 | 136 | 7za e cipher.zip '-p[12345678_23456789_34567890]' 137 | 138 | License: GNU Lesser General Public License v2.1 + unRAR restriction 139 | 140 | - [madler/infgen](https://github.com/madler/infgen/) 141 | 142 | Deflate disassembler to convert a deflate, zlib, or gzip stream into a readable form. 143 | 144 | License: zlib 145 | 146 | - [madler/spoof](https://github.com/madler/spoof) 147 | 148 | Modify a message to have a desired CRC signature. 149 | 150 | It can be used to reconstruct a plaintext file if only a few bytes are unknown using the CRC-32 checksum. 151 | 152 | License: zlib 153 | 154 | - [hannob/zipeinfo](https://github.com/hannob/zipeinfo) 155 | 156 | Python script telling which encryption method is used in a ZIP file. 157 | 158 | License: 0BSD 159 | 160 | - [pmqs/zipdetails](https://github.com/pmqs/zipdetails) 161 | 162 | Perl script displaying details about the internal structure of ZIP files. 163 | 164 | License: same as Perl - GNU General Public License or "Artistic License" 165 | -------------------------------------------------------------------------------- /include/bkcrack/Zip.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_ZIP_HPP 2 | #define BKCRACK_ZIP_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | /// \brief Open a zip archive, parse zip entries metadata and read raw content 11 | /// 12 | /// \note Zip64 extensions are supported. 13 | /// 14 | /// \limitation Spanned or split zip files are not supported. 15 | /// \limitation Strong encryption (SES) is not supported. 16 | /// In particular, central directory encryption is not supported. 17 | /// \limitation Language Encoding (EFS) is not supported. (\ref APPNOTE "APPNOTE.TXT", Appendix D) 18 | /// 19 | /// \see \ref APPNOTE "APPNOTE.TXT" 20 | class Zip 21 | { 22 | public: 23 | /// Exception thrown when parsing a zip file fails 24 | class Error : public BaseError 25 | { 26 | public: 27 | /// Constructor 28 | explicit Error(const std::string& description); 29 | }; 30 | 31 | /// Encryption algorithm 32 | enum class Encryption 33 | { 34 | None, ///< No encryption 35 | Traditional, ///< Traditional PKWARE encryption (ZipCrypto), vulnerable to known plaintext attack 36 | Unsupported ///< Other encryption (DES, RC2, 3DES, AES, Blowfish, Twofish, RC4) 37 | }; 38 | 39 | /// Compression algorithm. \note This enumeration is not exhaustive. 40 | enum class Compression 41 | { 42 | Store = 0, 43 | Shrink = 1, 44 | Implode = 6, 45 | Deflate = 8, 46 | Deflate64 = 9, 47 | BZip2 = 12, 48 | LZMA = 14, 49 | Zstandard = 93, 50 | MP3 = 94, 51 | XZ = 95, 52 | JPEG = 96, 53 | WavPack = 97, 54 | PPMd = 98, 55 | }; 56 | 57 | /// Information about a zip entry 58 | struct Entry 59 | { 60 | std::string name; ///< File name 61 | Encryption encryption; ///< Encryption method 62 | Compression compression; ///< Compression method. \note It may take a value not listed in Compression 63 | std::uint32_t crc32; ///< CRC-32 checksum 64 | std::uint64_t offset; ///< Offset of local file header 65 | std::uint64_t packedSize; ///< Packed data size 66 | std::uint64_t uncompressedSize; ///< Uncompressed data size 67 | std::uint8_t checkByte; ///< Last byte of the encryption header after decryption 68 | }; 69 | 70 | /// Single-pass input iterator that reads successive Entry objects 71 | class Iterator 72 | { 73 | public: 74 | /// @{ 75 | /// \brief Required types for iterators 76 | using difference_type = std::ptrdiff_t; 77 | using value_type = const Entry; 78 | using pointer = const Entry*; 79 | using reference = const Entry&; 80 | using iterator_category = std::input_iterator_tag; 81 | /// @} 82 | 83 | /// Construct end-of-stream iterator 84 | constexpr Iterator() noexcept = default; 85 | 86 | /// Construct an iterator pointing to the beginning of the given archive's central directory, 87 | /// or end-of-stream iterator if the central directory is not found at the expected offset. 88 | explicit Iterator(const Zip& archive); 89 | 90 | /// \brief Get the current entry 91 | /// \pre The iterator must be valid 92 | auto operator*() const -> const Entry& 93 | { 94 | return *m_entry; 95 | } 96 | 97 | /// \brief Access a member of the current entry 98 | /// \pre The iterator must be valid 99 | auto operator->() const -> const Entry* 100 | { 101 | return &(*m_entry); 102 | } 103 | 104 | /// \brief Read the next central directory record if any or assign end-of-stream iterator 105 | /// \pre The iterator must be valid 106 | auto operator++() -> Iterator&; 107 | 108 | /// \copydoc operator++ 109 | auto operator++(int) -> Iterator; 110 | 111 | /// Test if iterators are equivalent, i.e. both are end-of-stream or both are valid 112 | auto operator==(const Zip::Iterator& other) const -> bool 113 | { 114 | return (m_is == nullptr) == (other.m_is == nullptr); 115 | } 116 | 117 | /// Test if iterators are not equivalent 118 | auto operator!=(const Zip::Iterator& other) const -> bool 119 | { 120 | return !(*this == other); 121 | } 122 | 123 | private: 124 | std::istream* m_is = nullptr; 125 | std::optional m_entry; // optional type allows the end-of-stream iterator to be empty 126 | }; 127 | 128 | /// \brief Open a zip archive from an already opened input stream 129 | /// \exception Error if the given input stream is not a valid zip archive 130 | explicit Zip(std::istream& stream); 131 | 132 | /// Get an iterator pointing to the first entry 133 | auto begin() const -> Iterator 134 | { 135 | return Iterator{*this}; 136 | } 137 | 138 | /// Get an end-of-stream iterator 139 | auto end() const -> Iterator 140 | { 141 | return Iterator{}; 142 | } 143 | 144 | /// \brief Get the first entry having the given name 145 | /// \exception Error if the archive does not contain an entry with the given name 146 | auto operator[](const std::string& name) const -> Entry; 147 | 148 | /// \brief Get the entry at the given index 149 | /// \exception Error if the index is out of bounds 150 | auto operator[](std::size_t index) const -> Entry; 151 | 152 | /// \brief Check that the given entry uses the expected encryption algorithm 153 | /// \exception Error if the given entry does not use the expected encryption algorithm 154 | static void checkEncryption(const Entry& entry, Encryption expected); 155 | 156 | /// \brief Set the underlying stream's input position indicator at the beginning the given entry's raw data 157 | /// \exception Error if the given entry's data is not at the expected offset 158 | auto seek(const Entry& entry) const -> std::istream&; 159 | 160 | /// \brief Load at most \a count bytes of the given entry's raw data 161 | /// \exception Error if the given entry's data is not at the expected offset 162 | auto load(const Entry& entry, std::size_t count = std::numeric_limits::max()) const 163 | -> std::vector; 164 | 165 | /// \brief Copy the zip file into \a os changing the encrypted data using the given keys 166 | /// \exception Error if the archive is not a valid zip archive 167 | void changeKeys(std::ostream& os, const Keys& oldKeys, const Keys& newKeys, Progress& progress) const; 168 | 169 | /// \brief Copy the zip file into \a os removing encryption using the given keys 170 | /// \exception Error if the archive is not a valid zip archive 171 | void decrypt(std::ostream& os, const Keys& keys, Progress& progress) const; 172 | 173 | private: 174 | std::istream& m_is; 175 | const std::uint64_t m_centralDirectoryOffset; 176 | }; 177 | 178 | /// Decipher at most \a size bytes from \a is into \a os with the given keys. 179 | /// The first \a discard bytes are discarded. 180 | void decipher(std::istream& is, std::size_t size, std::size_t discard, std::ostream& os, Keys keys); 181 | 182 | #endif // BKCRACK_ZIP_HPP 183 | -------------------------------------------------------------------------------- /src/cli/Arguments.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BKCRACK_ARGUMENTS_HPP 2 | #define BKCRACK_ARGUMENTS_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /// Parse and store arguments 14 | class Arguments 15 | { 16 | public: 17 | /// Exception thrown if an argument is not valid 18 | class Error : public BaseError 19 | { 20 | public: 21 | /// Constructor 22 | explicit Error(const std::string& description); 23 | }; 24 | 25 | /// \brief Constructor parsing command line arguments 26 | /// \exception Error if an argument is not valid 27 | Arguments(int argc, const char* const argv[]); 28 | 29 | /// \brief Load the data needed for an attack based on parsed arguments 30 | /// \exception FileError if a file cannot be opened 31 | /// \exception ZipError if a zip entry cannot be opened 32 | /// \exception Data::Error if the loaded data cannot be used to carry out an attack 33 | auto loadData() const -> Data; 34 | 35 | std::optional cipherFile; ///< File containing the ciphertext 36 | std::optional cipherIndex; ///< Index of the zip entry containing ciphertext 37 | std::optional cipherArchive; ///< Zip archive containing \ref cipherFile 38 | 39 | std::optional plainFile; ///< File containing the known plaintext 40 | std::optional plainIndex; ///< Index of the zip entry containing plaintext 41 | std::optional plainArchive; ///< Zip archive containing \ref plainFile 42 | 43 | /// \brief Maximum number of bytes of plaintext to read from \ref plainFile 44 | /// 45 | /// Set to 1 MiB by default. Using more plaintext is possible, 46 | /// but it uses more RAM and does not speed up the attack much. 47 | std::size_t plainFilePrefix = 1 << 20; 48 | 49 | /// Plaintext offset relative to ciphertext without encryption header (may be negative) 50 | int offset = 0; 51 | 52 | /// Additional bytes of plaintext with their offset relative to ciphertext without encryption header (may be 53 | /// negative) 54 | std::map extraPlaintext; 55 | 56 | /// Tell not to use the check byte derived from ciphertext entry metadata as known plaintext 57 | bool ignoreCheckByte = false; 58 | 59 | /// Starting point of the attack on Z values remaining after reduction 60 | int attackStart = 0; 61 | 62 | /// Password from which to derive the internal password representation 63 | std::optional password; 64 | 65 | /// Internal password representation 66 | std::optional keys; 67 | 68 | /// File to write the deciphered text corresponding to \ref cipherFile 69 | std::optional decipheredFile; 70 | 71 | /// Tell whether to keep the encryption header or discard it when writing the deciphered text 72 | bool keepHeader = false; 73 | 74 | /// File to write an unencrypted copy of the encrypted archive 75 | std::optional decryptedArchive; 76 | 77 | /// Arguments needed to change an archive's password 78 | struct ChangePassword 79 | { 80 | std::string unlockedArchive; ///< File to write the new encrypted archive 81 | std::string newPassword; ///< Password chosen to generate the new archive 82 | }; 83 | /// \copydoc ChangePassword 84 | std::optional changePassword; 85 | 86 | /// \brief Arguments needed to change an archive's internal password representation 87 | /// 88 | /// Changing the internal password representation is an alternative to changing the password 89 | /// when the target password is not known, but its internal representation is known. 90 | struct ChangeKeys 91 | { 92 | std::string unlockedArchive; ///< File to write the new encrypted archive 93 | Keys newKeys; ///< Internal password representation chosen to generate the new archive 94 | }; 95 | /// \copydoc ChangeKeys 96 | std::optional changeKeys; 97 | 98 | /// Characters to generate password candidates 99 | std::optional> bruteforce; 100 | 101 | /// Range of password lengths to try during password recovery 102 | struct LengthInterval 103 | { 104 | /// Smallest password length to try (inclusive) 105 | std::size_t minLength{0}; 106 | 107 | /// Greatest password length to try (inclusive) 108 | std::size_t maxLength{std::numeric_limits::max()}; 109 | 110 | /// Compute the intersection between this interval and the given \a other interval 111 | auto operator&(const LengthInterval& other) const -> LengthInterval; 112 | }; 113 | /// \copydoc LengthInterval 114 | std::optional length; 115 | 116 | /// Mask for password recovery, alternative to bruteforce and length 117 | std::optional>> mask; 118 | 119 | /// Starting point for password recovery 120 | std::string recoveryStart; 121 | 122 | /// Number of threads to use for parallelized operations 123 | int jobs; 124 | 125 | /// Tell whether to try all candidates (keys or passwords) exhaustively or stop after the first success 126 | bool exhaustive = false; 127 | 128 | /// Zip archive about which to display information 129 | std::optional infoArchive; 130 | 131 | /// Tell whether version information is needed or not 132 | bool version = false; 133 | 134 | /// Tell whether help message is needed or not 135 | bool help = false; 136 | 137 | private: 138 | const char* const* m_current; 139 | const char* const* const m_end; 140 | 141 | std::unordered_map> m_charsets; 142 | std::unordered_map m_rawCharsets; 143 | 144 | // Resolve the set of characters denoted by the given charset specification, 145 | // recursively resolving referenced charsets from m_rawCharsets and caching results in m_charsets. 146 | auto resolveCharset(const std::string& rawCharset) -> std::bitset<256>; 147 | 148 | std::optional m_rawBruteforce; 149 | std::optional m_rawMask; 150 | 151 | auto finished() const -> bool; 152 | 153 | void parseArgument(); 154 | 155 | enum class Option 156 | { 157 | cipherFile, 158 | cipherIndex, 159 | cipherArchive, 160 | plainFile, 161 | plainIndex, 162 | plainArchive, 163 | plainFilePrefix, 164 | offset, 165 | extraPlaintext, 166 | ignoreCheckByte, 167 | attackStart, 168 | password, 169 | keys, 170 | decipheredFile, 171 | keepHeader, 172 | decryptedArchive, 173 | changePassword, 174 | changeKeys, 175 | bruteforce, 176 | length, 177 | recoverPassword, 178 | mask, 179 | charset, 180 | recoveryStart, 181 | jobs, 182 | exhaustive, 183 | infoArchive, 184 | version, 185 | help 186 | }; 187 | 188 | auto readString(const std::string& description) -> std::string; 189 | auto readOption(const std::string& description) -> Option; 190 | auto readInt(const std::string& description) -> int; 191 | auto readSize(const std::string& description) -> std::size_t; 192 | auto readHex(const std::string& description) -> std::vector; 193 | auto readKey(const std::string& description) -> std::uint32_t; 194 | auto readRawCharset(const std::string& description) -> std::string; 195 | }; 196 | 197 | #endif // BKCRACK_ARGUMENTS_HPP 198 | -------------------------------------------------------------------------------- /src/bkcrack/Data.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/Data.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | 12 | struct Range 13 | { 14 | auto size() const -> std::size_t 15 | { 16 | return std::distance(begin, end); 17 | } 18 | 19 | auto operator<(const Range& other) const -> bool 20 | { 21 | return size() < other.size(); 22 | } 23 | 24 | std::vector>::iterator begin; 25 | std::vector>::iterator end; 26 | }; 27 | 28 | } // namespace 29 | 30 | Data::Error::Error(const std::string& description) 31 | : BaseError{"Data error", description} 32 | { 33 | } 34 | 35 | Data::Data(std::vector ciphertextArg, std::vector plaintextArg, int offsetArg, 36 | const std::map& extraPlaintextArg) 37 | : ciphertext{std::move(ciphertextArg)} 38 | , plaintext{std::move(plaintextArg)} 39 | { 40 | // validate lengths 41 | if (ciphertext.size() < Attack::attackSize) 42 | throw Error{"ciphertext is too small for an attack (minimum length is " + std::to_string(Attack::attackSize) + 43 | ")"}; 44 | if (ciphertext.size() < plaintext.size()) 45 | throw Error{"ciphertext is smaller than plaintext"}; 46 | 47 | // validate offsets 48 | constexpr auto minimumOffset = -static_cast(encryptionHeaderSize); 49 | if (offsetArg < minimumOffset) 50 | throw Error{"plaintext offset " + std::to_string(offsetArg) + " is too small (minimum is " + 51 | std::to_string(minimumOffset) + ")"}; 52 | if (ciphertext.size() < encryptionHeaderSize + offsetArg + plaintext.size()) 53 | throw Error{"plaintext offset " + std::to_string(offsetArg) + " is too large"}; 54 | 55 | if (!extraPlaintextArg.empty() && extraPlaintextArg.begin()->first < minimumOffset) 56 | throw Error{"extra plaintext offset " + std::to_string(extraPlaintextArg.begin()->first) + 57 | " is too small (minimum is " + std::to_string(minimumOffset) + ")"}; 58 | if (!extraPlaintextArg.empty() && ciphertext.size() <= encryptionHeaderSize + extraPlaintextArg.rbegin()->first) 59 | throw Error{"extra plaintext offset " + std::to_string(extraPlaintextArg.rbegin()->first) + " is too large"}; 60 | 61 | // shift offsets to absolute values 62 | offset = encryptionHeaderSize + offsetArg; 63 | 64 | std::transform(extraPlaintextArg.begin(), extraPlaintextArg.end(), std::back_inserter(extraPlaintext), 65 | [](const std::pair& extra) { 66 | return std::pair{encryptionHeaderSize + extra.first, extra.second}; 67 | }); 68 | 69 | // merge contiguous plaintext with adjacent extra plaintext 70 | { 71 | // Split extra plaintext into three ranges: 72 | // - [extraPlaintext.begin(), before) before contiguous plaintext 73 | // - [before, after) overlapping contiguous plaintext 74 | // - [after, extraPlaintext.end()) after contiguous plaintext 75 | 76 | auto before = std::lower_bound(extraPlaintext.begin(), extraPlaintext.end(), std::pair{offset, std::uint8_t{}}); 77 | auto after = 78 | std::lower_bound(before, extraPlaintext.end(), std::pair{offset + plaintext.size(), std::uint8_t{}}); 79 | 80 | // overwrite overlapping plaintext 81 | std::for_each(before, after, 82 | [this](const std::pair& e) 83 | { plaintext[e.first - offset] = e.second; }); 84 | 85 | // merge contiguous plaintext with extra plaintext immediately before 86 | while (before != extraPlaintext.begin() && (before - 1)->first == offset - 1) 87 | { 88 | plaintext.insert(plaintext.begin(), (--before)->second); 89 | offset--; 90 | } 91 | 92 | // merge contiguous plaintext with extra plaintext immediately after 93 | while (after != extraPlaintext.end() && after->first == offset + plaintext.size()) 94 | plaintext.push_back((after++)->second); 95 | 96 | // discard merged extra plaintext 97 | extraPlaintext.erase(before, after); 98 | } 99 | 100 | // find the longest contiguous sequence in extra plaintext and use is as contiguous plaintext if sensible 101 | { 102 | auto range = Range{extraPlaintext.begin(), extraPlaintext.begin()}; // empty 103 | 104 | for (auto it = extraPlaintext.begin(); it != extraPlaintext.end();) 105 | { 106 | auto current = Range{it, ++it}; 107 | while (it != extraPlaintext.end() && it->first == (current.end - 1)->first + 1) 108 | current.end = ++it; 109 | 110 | range = std::max(range, current); 111 | } 112 | 113 | if (plaintext.size() < range.size()) 114 | { 115 | const auto plaintextSize = plaintext.size(); 116 | const auto rangeOffset = range.begin->first; 117 | 118 | // append last bytes from the range to contiguous plaintext 119 | for (auto i = plaintextSize; i < range.size(); i++) 120 | plaintext.push_back(range.begin[i].second); 121 | 122 | // remove those bytes from the range 123 | range.end = extraPlaintext.erase(range.begin + plaintextSize, range.end); 124 | if (plaintextSize == 0) 125 | range.begin = range.end; 126 | 127 | // rotate extra plaintext so that it will be sorted at the end of this scope 128 | { 129 | auto before = 130 | std::lower_bound(extraPlaintext.begin(), extraPlaintext.end(), std::pair{offset, std::uint8_t{}}); 131 | if (offset < rangeOffset) 132 | range = {before, std::rotate(before, range.begin, range.end)}; 133 | else 134 | range = {std::rotate(range.begin, range.end, before), before}; 135 | } 136 | 137 | // swap bytes between the former contiguous plaintext and the beginning of the range 138 | for (auto i = std::size_t{}; i < plaintextSize; i++) 139 | { 140 | range.begin[i].first = offset + i; 141 | std::swap(plaintext[i], range.begin[i].second); 142 | } 143 | 144 | offset = rangeOffset; 145 | } 146 | } 147 | 148 | // check that there is enough known plaintext 149 | if (plaintext.size() < Attack::contiguousSize) 150 | throw Error{"not enough contiguous plaintext (" + std::to_string(plaintext.size()) + 151 | " bytes available, minimum is " + std::to_string(Attack::contiguousSize) + ")"}; 152 | if (plaintext.size() + extraPlaintext.size() < Attack::attackSize) 153 | throw Error{"not enough plaintext (" + std::to_string(plaintext.size() + extraPlaintext.size()) + 154 | " bytes available, minimum is " + std::to_string(Attack::attackSize) + ")"}; 155 | 156 | // reorder remaining extra plaintext for filtering 157 | { 158 | auto before = std::lower_bound(extraPlaintext.begin(), extraPlaintext.end(), std::pair{offset, std::uint8_t{}}); 159 | std::reverse(extraPlaintext.begin(), before); 160 | std::inplace_merge( 161 | extraPlaintext.begin(), before, extraPlaintext.end(), 162 | [this](const std::pair& a, const std::pair& b) 163 | { 164 | constexpr auto absdiff = [](std::size_t x, std::size_t y) { return x < y ? y - x : x - y; }; 165 | return absdiff(a.first, offset + Attack::contiguousSize) < 166 | absdiff(b.first, offset + Attack::contiguousSize); 167 | }); 168 | } 169 | 170 | // compute keystream 171 | std::transform(plaintext.begin(), plaintext.end(), ciphertext.begin() + offset, std::back_inserter(keystream), 172 | std::bit_xor<>()); 173 | } 174 | -------------------------------------------------------------------------------- /src/bkcrack/Attack.cpp: -------------------------------------------------------------------------------- 1 | #include "bkcrack/Attack.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | Attack::Attack(const Data& data, std::size_t index, std::vector& solutions, std::mutex& solutionsMutex, 13 | bool exhaustive, Progress& progress) 14 | : data{data} 15 | , index{index + 1 - Attack::contiguousSize} 16 | , solutions{solutions} 17 | , solutionsMutex{solutionsMutex} 18 | , exhaustive{exhaustive} 19 | , progress{progress} 20 | { 21 | } 22 | 23 | void Attack::carryout(std::uint32_t z7_2_32) 24 | { 25 | zlist[7] = z7_2_32; 26 | exploreZlists(7); 27 | } 28 | 29 | void Attack::exploreZlists(int i) 30 | { 31 | if (i != 0) // the Z-list is not complete so generate Z{i-1}[2,32) values 32 | { 33 | // get Z{i-1}[10,32) from CRC32^-1 34 | const auto zim1_10_32 = Crc32Tab::getZim1_10_32(zlist[i]); 35 | 36 | // get Z{i-1}[2,16) values from keystream byte k{i-1} and Z{i-1}[10,16) 37 | for (const auto zim1_2_16 : KeystreamTab::getZi_2_16_vector(data.keystream[index + i - 1], zim1_10_32)) 38 | { 39 | // add Z{i-1}[2,32) to the Z-list 40 | zlist[i - 1] = zim1_10_32 | zim1_2_16; 41 | 42 | // find Zi[0,2) from CRC32^1 43 | zlist[i] &= mask<2, 32>; // discard 2 least significant bits 44 | zlist[i] |= (Crc32Tab::crc32inv(zlist[i], 0) ^ zlist[i - 1]) >> 8; 45 | 46 | // get Y{i+1}[24,32) 47 | if (i < 7) 48 | ylist[i + 1] = Crc32Tab::getYi_24_32(zlist[i + 1], zlist[i]); 49 | 50 | exploreZlists(i - 1); 51 | } 52 | } 53 | else // the Z-list is complete so iterate over possible Y values 54 | { 55 | // guess Y7[8,24) and keep prod == (Y7[8,32) - 1) * mult^-1 56 | for (auto y7_8_24 = std::uint32_t{}, prod = (MultTab::multInv * msb(ylist[7]) << 24) - MultTab::multInv; 57 | y7_8_24 < 1 << 24; y7_8_24 += 1 << 8, prod += MultTab::multInv << 8) 58 | // get possible Y7[0,8) values 59 | for (const auto y7_0_8 : MultTab::getMsbProdFiber3(msb(ylist[6]) - msb(prod))) 60 | // filter Y7[0,8) using Y6[24,32) 61 | if (prod + MultTab::multInv * y7_0_8 - (ylist[6] & mask<24, 32>) <= maxdiff<24>) 62 | { 63 | ylist[7] = y7_0_8 | y7_8_24 | (ylist[7] & mask<24, 32>); 64 | exploreYlists(7); 65 | } 66 | } 67 | } 68 | 69 | void Attack::exploreYlists(int i) 70 | { 71 | if (i != 3) // the Y-list is not complete so generate Y{i-1} values 72 | { 73 | const auto fy = (ylist[i] - 1) * MultTab::multInv; 74 | const auto ffy = (fy - 1) * MultTab::multInv; 75 | 76 | // get possible LSB(Xi) 77 | for (const auto xi_0_8 : MultTab::getMsbProdFiber2(msb(ffy - (ylist[i - 2] & mask<24, 32>)))) 78 | { 79 | // compute corresponding Y{i-1} 80 | const auto yim1 = fy - xi_0_8; 81 | 82 | // filter values with Y{i-2}[24,32) 83 | if (ffy - MultTab::multInv * xi_0_8 - (ylist[i - 2] & mask<24, 32>) <= maxdiff<24> && 84 | msb(yim1) == msb(ylist[i - 1])) 85 | { 86 | // add Y{i-1} to the Y-list 87 | ylist[i - 1] = yim1; 88 | 89 | // set Xi value 90 | xlist[i] = xi_0_8; 91 | 92 | exploreYlists(i - 1); 93 | } 94 | } 95 | } 96 | else // the Y-list is complete so check if the corresponding X-list is valid 97 | testXlist(); 98 | } 99 | 100 | void Attack::testXlist() 101 | { 102 | // compute X7 103 | for (auto i = 5; i <= 7; i++) 104 | xlist[i] = (Crc32Tab::crc32(xlist[i - 1], data.plaintext[index + i - 1]) & mask<8, 32>) // discard the LSB 105 | | lsb(xlist[i]); // set the LSB 106 | 107 | // compute X3 108 | auto x = xlist[7]; 109 | for (auto i = 6; i >= 3; i--) 110 | x = Crc32Tab::crc32inv(x, data.plaintext[index + i]); 111 | 112 | // check that X3 fits with Y1[26,32) 113 | const auto y1_26_32 = Crc32Tab::getYi_24_32(zlist[1], zlist[0]) & mask<26, 32>; 114 | if (((ylist[3] - 1) * MultTab::multInv - lsb(x) - 1) * MultTab::multInv - y1_26_32 > maxdiff<26>) 115 | return; 116 | 117 | // decipher and filter by comparing with remaining contiguous plaintext forward 118 | auto keysForward = Keys{xlist[7], ylist[7], zlist[7]}; 119 | keysForward.update(data.plaintext[index + 7]); 120 | for (auto p = data.plaintext.begin() + index + 8, c = data.ciphertext.begin() + data.offset + index + 8; 121 | p != data.plaintext.end(); ++p, ++c) 122 | { 123 | if ((*c ^ keysForward.getK()) != *p) 124 | return; 125 | keysForward.update(*p); 126 | } 127 | 128 | auto indexForward = data.offset + data.plaintext.size(); 129 | 130 | // and also backward 131 | auto keysBackward = Keys{x, ylist[3], zlist[3]}; 132 | for (auto p = std::reverse_iterator{data.plaintext.begin() + index + 3}, 133 | c = std::reverse_iterator{data.ciphertext.begin() + data.offset + index + 3}; 134 | p != data.plaintext.rend(); ++p, ++c) 135 | { 136 | keysBackward.updateBackward(*c); 137 | if ((*c ^ keysBackward.getK()) != *p) 138 | return; 139 | } 140 | 141 | auto indexBackward = data.offset; 142 | 143 | // continue filtering with extra known plaintext 144 | for (const auto& [extraIndex, extraByte] : data.extraPlaintext) 145 | { 146 | auto p = std::uint8_t{}; 147 | if (extraIndex < indexBackward) 148 | { 149 | keysBackward.updateBackward(data.ciphertext, indexBackward, extraIndex); 150 | indexBackward = extraIndex; 151 | p = data.ciphertext[indexBackward] ^ keysBackward.getK(); 152 | } 153 | else 154 | { 155 | keysForward.update(data.ciphertext, indexForward, extraIndex); 156 | indexForward = extraIndex; 157 | p = data.ciphertext[indexForward] ^ keysForward.getK(); 158 | } 159 | 160 | if (p != extraByte) 161 | return; 162 | } 163 | 164 | // all tests passed so the keys are found 165 | 166 | // get the keys associated with the initial state 167 | keysBackward.updateBackward(data.ciphertext, indexBackward, 0); 168 | 169 | { 170 | const auto lock = std::scoped_lock{solutionsMutex}; 171 | solutions.push_back(keysBackward); 172 | } 173 | 174 | progress.log([&keysBackward](std::ostream& os) { os << "Keys: " << keysBackward << std::endl; }); 175 | 176 | if (!exhaustive) 177 | progress.state = Progress::State::EarlyExit; 178 | } 179 | 180 | auto attack(const Data& data, const std::vector& zi_2_32_vector, int& start, std::size_t index, int jobs, 181 | const bool exhaustive, Progress& progress) -> std::vector 182 | { 183 | const auto* candidates = zi_2_32_vector.data(); 184 | const auto size = static_cast(zi_2_32_vector.size()); 185 | 186 | auto solutions = std::vector{}; 187 | auto solutionsMutex = std::mutex{}; 188 | auto worker = Attack{data, index, solutions, solutionsMutex, exhaustive, progress}; 189 | 190 | progress.done = start; 191 | progress.total = size; 192 | 193 | const auto threadCount = std::clamp(jobs, 1, size); 194 | auto threads = std::vector{}; 195 | auto nextCandidateIndex = std::atomic{start}; 196 | for (auto i = 0; i < threadCount; ++i) 197 | threads.emplace_back( 198 | [&nextCandidateIndex, size, &progress, candidates, worker]() mutable 199 | { 200 | for (auto i = nextCandidateIndex++; i < size; i = nextCandidateIndex++) 201 | { 202 | worker.carryout(candidates[i]); 203 | progress.done++; 204 | 205 | if (progress.state != Progress::State::Normal) 206 | break; 207 | } 208 | }); 209 | for (auto& thread : threads) 210 | thread.join(); 211 | 212 | start = std::min(nextCandidateIndex.load(), size); 213 | 214 | return solutions; 215 | } 216 | -------------------------------------------------------------------------------- /example/tutorial.md: -------------------------------------------------------------------------------- 1 | Tutorial {#tutorial} 2 | ======== 3 | 4 | \brief A guide to crack an example encrypted zip file. 5 | 6 | The `example` folder contains an example zip file `secrets.zip` so you can run an attack. 7 | Its content is probably of great interest! 8 | 9 | # What is inside 10 | 11 | Let us see what is inside. 12 | Open a terminal in the `example` folder and run this command. 13 | 14 | $ ../bkcrack -L secrets.zip 15 | 16 | We get the following output. 17 | 18 | Archive: secrets.zip 19 | Index Encryption Compression CRC32 Uncompressed Packed size Name 20 | ----- ---------- ----------- -------- ------------ ------------ ---------------- 21 | 0 ZipCrypto Deflate 7ca9f10a 54799 54700 advice.jpg 22 | 1 ZipCrypto Store a99f1d0d 1265 1277 spiral.svg 23 | 24 | So the zip file contains two entries: `advice.jpg` and `spiral.svg`. 25 | They are both encrypted with traditional PKWARE encryption denoted as ZipCrypto. 26 | We also see that `advice.jpg` is deflated whereas `spiral.svg` is stored uncompressed. 27 | 28 | # Guessing plaintext 29 | 30 | To run the attack, we must guess at least 12 bytes of plaintext. 31 | On average, the more plaintext we guess, the faster the attack will be. 32 | 33 | ## The easy way: stored file 34 | 35 | We can guess from its extension that `spiral.svg` probably starts with the string ` plain.txt 66 | 67 | We are now ready to run the attack. 68 | 69 | $ ../bkcrack -C secrets.zip -c spiral.svg -p plain.txt 70 | 71 | After a little while, the keys will appear! 72 | 73 | [17:42:43] Z reduction using 13 bytes of known plaintext 74 | 100.0 % (13 / 13) 75 | [17:42:44] Attack on 542303 Z values at index 6 76 | Keys: c4490e28 b414a23d 91404b31 77 | 33.9 % (183750 / 542303) 78 | Found a solution. Stopping. 79 | You may resume the attack with the option: --continue-attack 183750 80 | [17:48:03] Keys 81 | c4490e28 b414a23d 91404b31 82 | 83 | # Recovering the original files 84 | 85 | Once we have the keys, we can recover the original files. 86 | 87 | ## Remove the password 88 | 89 | We assume that the same keys were used for all the files in the zip file. 90 | We can create a new archive based on `secrets.zip`, but without password protection. 91 | 92 | $ ../bkcrack -C secrets.zip -k c4490e28 b414a23d 91404b31 -D secrets_without_password.zip 93 | 94 | Then, any zip file utility can extract the created archive. 95 | 96 | ## Choose a new password 97 | 98 | We can also create a new encrypted archive, but with a new password, `easy` in this example. 99 | 100 | $ ../bkcrack -C secrets.zip -k c4490e28 b414a23d 91404b31 -U secrets_with_new_password.zip easy 101 | 102 | Then, you will just have to type the chosen password when prompted to extract the created archive. 103 | 104 | ## Or decipher files 105 | 106 | Alternatively, we can decipher files one by one. 107 | 108 | $ ../bkcrack -C secrets.zip -c spiral.svg -k c4490e28 b414a23d 91404b31 -d spiral_deciphered.svg 109 | 110 | The file `spiral.svg` was stored uncompressed so we are done. 111 | 112 | $ ../bkcrack -C secrets.zip -c advice.jpg -k c4490e28 b414a23d 91404b31 -d advice_deciphered.deflate 113 | 114 | The file `advice.jpg` was compressed with the deflate algorithm in the zip file, so we now have to uncompressed it. 115 | 116 | A python script is provided for this purpose in the `tools` folder. 117 | 118 | $ python3 ../tools/inflate.py < advice_deciphered.deflate > very_good_advice.jpg 119 | 120 | You can now open `very_good_advice.jpg` and enjoy it! 121 | 122 | # Recovering the original password 123 | 124 | As shown above, the original password is not required to decrypt data. 125 | The internal keys are enough. 126 | However, we might also be interested in finding the original password. 127 | 128 | ## Bruteforce password recovery 129 | 130 | To do this, we need to choose a maximum length and a set of characters among which we hope to find those that constitute the password. 131 | To save time, we have to choose those parameters wisely. 132 | For a given length, a small charset will be explored much faster than a big one, but making a wrong assumption by choosing a charset that is too small will not allow to recover the password. 133 | 134 | At first, we can try all candidates up to a given length without making any assumption about the character set. 135 | We use the charset `?b` which is the set containing all bytes (from 0 to 255), so we do not miss any candidate up to length 9. 136 | 137 | $ ../bkcrack -k c4490e28 b414a23d 91404b31 --bruteforce ?b --length 0..9 138 | 139 | [17:52:16] Recovering password 140 | length 0-6... 141 | length 7... 142 | length 8... 143 | length 9... 144 | [17:52:16] Could not recover password 145 | 146 | It failed so we know the password has 10 characters or more. 147 | 148 | Now, let us assume the password is made of 10 or 11 printable ASCII characters, using the charset `?p`. 149 | 150 | $ ../bkcrack -k c4490e28 b414a23d 91404b31 --bruteforce ?p --length 10..11 151 | 152 | [17:52:34] Recovering password 153 | length 10... 154 | length 11... 155 | 100.0 % (9025 / 9025) 156 | [17:52:38] Could not recover password 157 | 158 | It failed again so we know the password has non-printable ASCII characters or has 12 or more characters. 159 | 160 | Now, let us assume the password is made of 12 alpha-numerical characters. 161 | 162 | $ ../bkcrack -k c4490e28 b414a23d 91404b31 --bruteforce ?a --length 12 163 | 164 | [17:54:37] Recovering password 165 | length 12... 166 | Password: W4sF0rgotten 167 | 51.7 % (1989 / 3844) 168 | Found a solution. Stopping. 169 | You may resume the password recovery with the option: --continue-recovery 573478303030 170 | [17:54:49] Password 171 | as bytes: 57 34 73 46 30 72 67 6f 74 74 65 6e 172 | as text: W4sF0rgotten 173 | 174 | Tada! We made the right assumption for this case. 175 | The password was recovered quickly from the keys. 176 | 177 | ## Mask-based password recovery 178 | 179 | This case was easy enough, but some passwords are too long for bruteforce to be viable. 180 | For such long passwords, it is worth trying to restrict the search space. 181 | Instead of using the same charset to draw all characters, we can specify a charset for each character in the password. 182 | This sequence of charsets is the mask. 183 | The mask must be chosen carefully to be large enough to contain the password but small enough to be explored in a reasonable amount of time. 184 | 185 | Here is an example. 186 | Assume a known-plaintext attack gave us the keys `b8c377a6 f603160f 1832a78b`. 187 | Now we want to find the password. 188 | Lucky for us, we remember vaguely that our password is made of 10 letters (uppercase or lowercase) and 5 binary digits. 189 | 190 | Recovering the password with bruteforce could take *days*: 191 | 192 | $ ../bkcrack -k b8c377a6 f603160f 1832a78b --bruteforce ?u?l01 --length 15 193 | 194 | Instead, we can take advantage of our partial knowledge of the password to significantly narrow down the search space: 195 | 196 | $ ../bkcrack -k b8c377a6 f603160f 1832a78b --mask ?x?x?x?x?x?x?x?x?x?x?y?y?y?y?y -s x ?u?l -s y 01 197 | 198 | [17:56:08] Recovering password 199 | Password: VerySecret01011 200 | 82.9 % (1379 / 1664) 201 | Found a solution. Stopping. 202 | You may resume the password recovery with the option: --continue-recovery 313130313062414141 203 | [17:56:08] Password 204 | as bytes: 56 65 72 79 53 65 63 72 65 74 30 31 30 31 31 205 | as text: VerySecret01011 206 | 207 | This command searches for a password where the first 10 characters are from charset `?x` (a custom charset defined as `?u?l` for uppercase or lowercase letters) and the next 5 characters are from charset `?y` (a custom charset defined as `01` for binary digits). 208 | In this example, restricting the search space that way makes the recovery run and find the password in milliseconds. 209 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | bkcrack 2 | ======= 3 | 4 | [![CI badge](https://github.com/kimci86/bkcrack/actions/workflows/ci.yml/badge.svg)](https://github.com/kimci86/bkcrack/actions/workflows/ci.yml) 5 | [![coverage badge](https://coveralls.io/repos/github/kimci86/bkcrack/badge.svg)](https://coveralls.io/github/kimci86/bkcrack) 6 | [![release badge](https://img.shields.io/github/v/release/kimci86/bkcrack)](https://github.com/kimci86/bkcrack/releases) 7 | [![license badge](https://img.shields.io/github/license/kimci86/bkcrack?color=informational)](license.txt) 8 | [![GitHub Sponsors badge](https://img.shields.io/github/sponsors/kimci86?color=red)](https://github.com/sponsors/kimci86) 9 | 10 | Crack legacy zip encryption with Biham and Kocher's known plaintext attack. 11 | 12 | Overview 13 | -------- 14 | 15 | A ZIP archive may contain many entries whose content can be compressed and/or encrypted. 16 | In particular, entries can be encrypted with a password-based symmetric encryption algorithm referred to as traditional PKWARE encryption, legacy encryption or ZipCrypto. 17 | This algorithm generates a pseudo-random stream of bytes (keystream) which is XORed to the entry's content (plaintext) to produce encrypted data (ciphertext). 18 | The generator's state, made of three 32-bits integers, is initialized using the password and then continuously updated with plaintext as encryption goes on. 19 | This encryption algorithm is vulnerable to known plaintext attacks as shown by Eli Biham and Paul C. Kocher in the research paper [A known plaintext attack on the PKZIP stream cipher](https://doi.org/10.1007/3-540-60590-8_12). 20 | Given ciphertext and 12 or more bytes of the corresponding plaintext, the internal state of the keystream generator can be recovered. 21 | This internal state is enough to decipher ciphertext entirely as well as other entries which were encrypted with the same password. 22 | It can also be used to bruteforce the password with a complexity of *nl-6* where *n* is the size of the character set and *l* is the length of the password. 23 | 24 | **bkcrack** is a command-line tool which implements this known plaintext attack. 25 | The main features are: 26 | 27 | - Recover internal state from ciphertext and plaintext. 28 | - Remove or change a ZIP archive's password using the internal state. 29 | - Recover the original password from the internal state. 30 | 31 | Install 32 | ------- 33 | 34 | ### Precompiled packages 35 | 36 | You can get the latest official release on [GitHub](https://github.com/kimci86/bkcrack/releases). 37 | 38 | Precompiled packages for Ubuntu, MacOS and Windows are available for download. 39 | Extract the downloaded archive wherever you like. 40 | 41 | On Windows, Microsoft runtime libraries are needed for bkcrack to run. 42 | If they are not already installed on your system, download and install the latest Microsoft Visual C++ Redistributable package. 43 | 44 | ### Compile from source 45 | 46 | Alternatively, you can compile the project with [CMake](https://cmake.org). 47 | 48 | First, download the source files or clone the git repository. 49 | Then, running the following commands in the source tree will create an installation in the `install` folder. 50 | 51 | ``` 52 | cmake -S . -B build -DCMAKE_INSTALL_PREFIX=install 53 | cmake --build build --config Release 54 | cmake --build build --config Release --target install 55 | ``` 56 | 57 | ### Third-party packages 58 | 59 | bkcrack is available in the package repositories listed below. 60 | Those packages are provided by external maintainers. 61 | 62 | [![Packaging status](https://repology.org/badge/vertical-allrepos/bkcrack.svg)](https://repology.org/project/bkcrack/versions) 63 | 64 | Usage 65 | ----- 66 | 67 | ### List entries 68 | 69 | You can see a list of entry names and metadata in an archive named `archive.zip` like this: 70 | 71 | bkcrack -L archive.zip 72 | 73 | Entries using ZipCrypto encryption are vulnerable to a known-plaintext attack. 74 | 75 | ### Recover internal keys 76 | 77 | The attack requires at least 12 bytes of known plaintext. 78 | At least 8 of them must be contiguous. 79 | The larger the contiguous known plaintext, the faster the attack. 80 | 81 | #### Load data from zip archives 82 | 83 | Having a zip archive `encrypted.zip` with the entry `cipher` being the ciphertext and `plain.zip` with the entry `plain` as the known plaintext, bkcrack can be run like this: 84 | 85 | bkcrack -C encrypted.zip -c cipher -P plain.zip -p plain 86 | 87 | #### Load data from files 88 | 89 | Having a file `cipherfile` with the ciphertext (starting with the 12 bytes corresponding to the encryption header) and `plainfile` with the known plaintext, bkcrack can be run like this: 90 | 91 | bkcrack -c cipherfile -p plainfile 92 | 93 | #### Offset 94 | 95 | If the plaintext corresponds to a part other than the beginning of the ciphertext, you can specify an offset. 96 | It can be negative if the plaintext includes a part of the encryption header. 97 | 98 | bkcrack -c cipherfile -p plainfile -o offset 99 | 100 | #### Sparse plaintext 101 | 102 | If you know little contiguous plaintext (between 8 and 11 bytes), but know some bytes at some other known offsets, you can provide this information to reach the requirement of a total of 12 known bytes. 103 | To do so, use the `-x` flag followed by an offset and bytes in hexadecimal. 104 | 105 | bkcrack -c cipherfile -p plainfile -x 25 4b4f -x 30 21 106 | 107 | ### Decipher 108 | 109 | If the attack is successful, the deciphered data associated to the ciphertext used for the attack can be saved: 110 | 111 | bkcrack -c cipherfile -p plainfile -d decipheredfile 112 | 113 | If the keys are known from a previous attack, it is possible to use bkcrack to decipher data: 114 | 115 | bkcrack -c cipherfile -k 12345678 23456789 34567890 -d decipheredfile 116 | 117 | #### Decompress 118 | 119 | The deciphered data might be compressed depending on whether compression was used or not when the zip file was created. 120 | If deflate compression was used, a Python 3 script provided in the `tools` folder may be used to decompress data. 121 | 122 | python3 tools/inflate.py < decipheredfile > decompressedfile 123 | 124 | ### Remove password 125 | 126 | To get access to all the entries of the encrypted archive in a single step, you can generate a new archive with the same content but without encryption. 127 | It assumes that every entry was originally encrypted with the same password. 128 | 129 | bkcrack -C encrypted.zip -k 12345678 23456789 34567890 -D decrypted.zip 130 | 131 | ### Change password 132 | 133 | It is also possible to generate a new encrypted archive with the password of your choice: 134 | 135 | bkcrack -C encrypted.zip -k 12345678 23456789 34567890 -U unlocked.zip new_password 136 | 137 | You can also define the new password by its corresponding internal representation. 138 | 139 | bkcrack -C encrypted.zip -k 12345678 23456789 34567890 --change-keys unlocked.zip 581da44e 8e40167f 50c009a0 140 | 141 | Those two commands can be used together to change the contents of an encrypted archive without knowing the password but knowing only the internal keys: 142 | you can make a copy encrypted with the password of you choice, 143 | then edit the copy with an archive manager entering the chosen password when prompted, 144 | and finally make a copy of the modified archive back with the original encryption keys. 145 | 146 | ### Recover password 147 | 148 | Given the internal keys, bkcrack can try to find the original password. 149 | 150 | #### Bruteforce password recovery 151 | 152 | You can look for a password using characters in a given charset: 153 | 154 | bkcrack -k 1ded830c 24454157 7213b8c5 -b ?p 155 | 156 | You can restrict the search to passwords of a given length or a range of lengths: 157 | 158 | bkcrack -k 1ded830c 24454157 7213b8c5 -b ?p -l 9 159 | bkcrack -k 1ded830c 24454157 7213b8c5 -b ?p -l 8..10 160 | 161 | Option `-r ` is a shortcut for `-l 0.. -b `: 162 | 163 | bkcrack -k 1ded830c 24454157 7213b8c5 -r 10 ?p 164 | 165 | #### Mask-based password recovery 166 | 167 | If you have some knowledge about how the password is formed, you can specify a mask to restrict the search space and make the recovery much faster. 168 | This is relevant for long passwords (such as those with 12 or more characters) for which bruteforce becomes very time-consuming. 169 | 170 | For example, assuming you vaguely remember that your password is made of 8 lowercase letters, an hyphen and 6 decimal digits, you can use this command: 171 | 172 | bkcrack -k 1940e266 d3fd3d89 71ce9871 -m ?l?l?l?l?l?l?l?l-?d?d?d?d?d?d 173 | 174 | This runs in milliseconds, whereas the bruteforce alternative takes hours. 175 | 176 | #### Character sets 177 | 178 | The search space for both bruteforce and mask-based password recovery is defined with charsets. 179 | A charset is a sequence of characters or shortcuts for existing charsets. 180 | Predefined charsets are listed below. 181 | 182 | Shortcut | Description | Value 183 | ---------|----------------------------|--------------------------------------- 184 | `?l` | lowercase letters | `abcdefghijklmnopqrstuvwxyz` 185 | `?u` | uppercase letters | `ABCDEFGHIJKLMNOPQRSTUVWXYZ` 186 | `?d` | decimal digits | `0123456789` 187 | `?s` | special characters | `` !"#$%&'()*+,-./:;<=>?@[\]^_`{\|}~`` 188 | `?a` | alpha-numerical characters | same as `?l?u?d` 189 | `?p` | printable ASCII characters | same as `?l?u?d?s` 190 | `?b` | all bytes | `0x00` .. `0xff` 191 | 192 | In addition to predefined charsets, you can define custom charsets with the `-s` option. 193 | Custom charsets can reference predefined charsets or other custom charsets. 194 | Custom charsets are especially useful for precise specification of the mask-based recovery search space. 195 | 196 | For example, if you know your password is made of 10 letters (uppercase or lowercase) and 5 binary digits, you can use this command: 197 | 198 | bkcrack -k b8c377a6 f603160f 1832a78b -m ?x?x?x?x?x?x?x?x?x?x?y?y?y?y?y -s x ?u?l -s y 01 199 | 200 | Learn 201 | ----- 202 | 203 | A tutorial is provided in the `example` folder. 204 | 205 | For more information, have a look at the documentation and read the source. 206 | 207 | Contribute 208 | ---------- 209 | 210 | Do not hesitate to suggest improvements or submit pull requests on [GitHub](https://github.com/kimci86/bkcrack). 211 | 212 | If you would like to show your support to the project, you are welcome to make a donation or sponsor the project via [Github Sponsors](https://github.com/sponsors/kimci86). 213 | 214 | License 215 | ------- 216 | 217 | This project is provided under the terms of the [zlib/png license](http://opensource.org/licenses/Zlib). 218 | -------------------------------------------------------------------------------- /tests/bkcrack/password.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | auto makeCharset(std::uint8_t front, std::uint8_t back) 12 | { 13 | auto vector = std::vector(back - front + 1); 14 | std::iota(vector.begin(), vector.end(), front); 15 | return vector; 16 | } 17 | auto charsetUnion(const std::vector& charsetA, const std::vector& charsetB) 18 | { 19 | auto vector = std::vector{}; 20 | for (auto c = 0; c < 256; ++c) 21 | { 22 | if (std::find(charsetA.begin(), charsetA.end(), c) != charsetA.end() || 23 | std::find(charsetB.begin(), charsetB.end(), c) != charsetB.end()) 24 | vector.push_back(c); 25 | } 26 | return vector; 27 | } 28 | auto charsetDifference(const std::vector& charsetA, const std::vector& charsetB) 29 | { 30 | auto vector = std::vector{}; 31 | for (const auto c : charsetA) 32 | if (std::find(charsetB.begin(), charsetB.end(), c) == charsetB.end()) 33 | vector.push_back(c); 34 | return vector; 35 | } 36 | const auto b = makeCharset(0, 255); 37 | const auto p = makeCharset(' ', '~'); 38 | const auto d = makeCharset('0', '9'); 39 | const auto u = makeCharset('A', 'Z'); 40 | const auto l = makeCharset('a', 'z'); 41 | const auto a = charsetUnion(charsetUnion(l, u), d); 42 | const auto s = charsetDifference(p, a); 43 | const auto sl = charsetUnion(s, l); 44 | } // namespace 45 | 46 | TEST("bruteforce empty password") 47 | { 48 | auto start = std::string{}; 49 | auto os = std::ostringstream{}; 50 | auto progress = Progress{os}; 51 | const auto result = recoverPassword(Keys{}, l, 0, 8, start, 1, false, progress); 52 | 53 | CHECK(result.size() == 1); 54 | CHECK(result[0] == ""); 55 | 56 | CHECK(start == ""); 57 | CHECK(progress.done == 0); 58 | CHECK(progress.total == 0); 59 | CHECK(progress.state == Progress::State::EarlyExit); 60 | } 61 | 62 | TEST("bruteforce password of 4 bytes") 63 | { 64 | auto start = std::string{}; 65 | auto os = std::ostringstream{}; 66 | auto progress = Progress{os}; 67 | const auto result = recoverPassword({0x1b226dfe, 0xc089e0a3, 0x6af00ee6}, b, 0, 4, start, 1, false, progress); 68 | 69 | CHECK(result.size() == 1); 70 | CHECK(result[0] == "🔐"); 71 | 72 | CHECK(start == ""); 73 | CHECK(progress.done == 0); 74 | CHECK(progress.total == 0); 75 | CHECK(progress.state == Progress::State::EarlyExit); 76 | } 77 | 78 | TEST("bruteforce password of 8 characters") 79 | { 80 | auto start = std::string{}; 81 | auto os = std::ostringstream{}; 82 | auto progress = Progress{os}; 83 | const auto result = recoverPassword({0x9bcb20c6, 0x10a97ca5, 0x103c0614}, p, 0, 8, start, 1, false, progress); 84 | 85 | CHECK(result.size() == 1); 86 | CHECK(result[0] == "_S#cr3t!"); 87 | 88 | CHECK(start == ""); 89 | CHECK(progress.done == 0); 90 | CHECK(progress.total == 0); 91 | CHECK(progress.state == Progress::State::EarlyExit); 92 | } 93 | 94 | TEST("bruteforce password of 10 characters") 95 | { 96 | auto start = std::string{}; 97 | auto os = std::ostringstream{}; 98 | auto progress = Progress{os}; 99 | const auto result = recoverPassword({0xedb43a00, 0x9ce6e179, 0x8cf2cbba}, a, 0, 10, start, 1, false, progress); 100 | 101 | CHECK(result.size() == 1); 102 | CHECK(result[0] == "q1w2e3r4t5"); 103 | 104 | CHECK(start == "r000"); 105 | CHECK(progress.done == 53 * 62 + 0); 106 | CHECK(progress.total == 62 * 62); 107 | CHECK(progress.state == Progress::State::EarlyExit); 108 | } 109 | 110 | TEST("bruteforce password of 12 characters") 111 | { 112 | auto start = std::string{}; 113 | auto os = std::ostringstream{}; 114 | auto progress = Progress{os}; 115 | const auto result = recoverPassword({0xdcce7593, 0xb8a2e617, 0xb2bd4365}, l, 0, 12, start, 1, false, progress); 116 | 117 | CHECK(result.size() == 1); 118 | CHECK(result[0] == "abcdefghijkl"); 119 | 120 | CHECK(start == "abdaaa"); 121 | CHECK(progress.done == 0 * 26 + 1); 122 | CHECK(progress.total == 26 * 26); 123 | CHECK(progress.state == Progress::State::EarlyExit); 124 | } 125 | 126 | TEST("bruteforce password of 14 characters with restart point") 127 | { 128 | auto start = std::string{"mmzzzaaa"}; 129 | auto os = std::ostringstream{}; 130 | auto progress = Progress{os}; 131 | const auto result = recoverPassword({0x1272ef9e, 0x20884732, 0x7a39ab85}, l, 0, 14, start, 1, false, progress); 132 | 133 | CHECK(result.size() == 1); 134 | CHECK(result[0] == "mnbzzghijklmno"); 135 | 136 | CHECK(start == "mncaaaaa"); 137 | CHECK(progress.done == 12 * 26 + 13); 138 | CHECK(progress.total == 26 * 26); 139 | CHECK(progress.state == Progress::State::EarlyExit); 140 | } 141 | 142 | TEST("exhaustive bruteforce") 143 | { 144 | auto start = std::string{}; 145 | auto os = std::ostringstream{}; 146 | auto progress = Progress{os}; 147 | const auto result = recoverPassword({0xedb43a00, 0x9ce6e179, 0x8cf2cbba}, a, 0, 10, start, 1, true, progress); 148 | 149 | CHECK(result.size() == 1); 150 | CHECK(result[0] == "q1w2e3r4t5"); 151 | 152 | CHECK(start == ""); 153 | CHECK(progress.done == 62 * 62); 154 | CHECK(progress.total == 62 * 62); 155 | CHECK(progress.state == Progress::State::Normal); 156 | } 157 | 158 | TEST("bruteforce attempt with solution out of given charset") 159 | { 160 | auto start = std::string{}; 161 | auto os = std::ostringstream{}; 162 | auto progress = Progress{os}; 163 | const auto result = recoverPassword({0xf1f9ab49, 0x8574a6fd, 0xcb99758d}, d, 0, 6, start, 1, false, progress); 164 | 165 | CHECK(result.empty()); 166 | 167 | CHECK(start == ""); 168 | CHECK(progress.done == 0); 169 | CHECK(progress.total == 0); 170 | CHECK(progress.state == Progress::State::Normal); 171 | CHECK(os.str().find("Password: 123x56 (as bytes: 31 32 33 78 35 36)") != std::string::npos); 172 | } 173 | 174 | TEST("mask-based recovery on empty password") 175 | { 176 | auto start = std::string{}; 177 | auto os = std::ostringstream{}; 178 | auto progress = Progress{os}; 179 | const auto result = recoverPassword(Keys{}, {}, start, 1, false, progress); 180 | 181 | CHECK(result.size() == 1); 182 | CHECK(result[0] == ""); 183 | 184 | CHECK(start == ""); 185 | CHECK(progress.done == 0); 186 | CHECK(progress.total == 0); 187 | CHECK(progress.state == Progress::State::EarlyExit); 188 | } 189 | 190 | TEST("mask-based recovery on password of 5 characters") 191 | { 192 | auto start = std::string{}; 193 | auto os = std::ostringstream{}; 194 | auto progress = Progress{os}; 195 | const auto result = 196 | recoverPassword({0x5e07e483, 0x0c4900a4, 0x4e586ac1}, {u, l, d, s, {'.'}}, start, 1, false, progress); 197 | 198 | CHECK(result.size() == 1); 199 | CHECK(result[0] == "Aa1_."); 200 | 201 | CHECK(start == ""); 202 | CHECK(progress.done == 0); 203 | CHECK(progress.total == 0); 204 | CHECK(progress.state == Progress::State::EarlyExit); 205 | } 206 | 207 | TEST("mask-based recovery on password of 6 characters") 208 | { 209 | auto start = std::string{}; 210 | auto os = std::ostringstream{}; 211 | auto progress = Progress{os}; 212 | const auto result = 213 | recoverPassword({0xf9720e40, 0x2520f2b9, 0x0a5660df}, {d, l, d, l, d, l}, start, 1, false, progress); 214 | 215 | CHECK(result.size() == 1); 216 | CHECK(result[0] == "1q2w3e"); 217 | 218 | CHECK(start == ""); 219 | CHECK(progress.done == 0); 220 | CHECK(progress.total == 0); 221 | CHECK(progress.state == Progress::State::EarlyExit); 222 | } 223 | 224 | TEST("mask-based recovery on password of 7 characters") 225 | { 226 | auto start = std::string{}; 227 | auto os = std::ostringstream{}; 228 | auto progress = Progress{os}; 229 | const auto result = 230 | recoverPassword({0x2af9b027, 0x85bd8154, 0x286ca64f}, {l, l, l, l, l, l, l}, start, 1, false, progress); 231 | 232 | CHECK(result.size() == 1); 233 | CHECK(result[0] == "letmein"); 234 | 235 | CHECK(start == ""); 236 | CHECK(progress.done == 0); 237 | CHECK(progress.total == 0); 238 | CHECK(progress.state == Progress::State::EarlyExit); 239 | } 240 | 241 | TEST("mask-based recovery on password of 13 characters") 242 | { 243 | auto start = std::string{}; 244 | auto os = std::ostringstream{}; 245 | auto progress = Progress{os}; 246 | const auto result = recoverPassword({0x0d892b8b, 0x02dd8fad, 0x77f52c7b}, 247 | {l, l, l, l, l, l, l, l, {'-'}, d, d, d, d}, start, 1, false, progress); 248 | 249 | CHECK(result.size() == 1); 250 | CHECK(result[0] == "password-1234"); 251 | 252 | CHECK(start == "4400-aa"); 253 | CHECK(progress.done == 44); 254 | CHECK(progress.total == 100); 255 | CHECK(progress.state == Progress::State::EarlyExit); 256 | } 257 | 258 | TEST("mask-based recovery with restart point") 259 | { 260 | auto start = std::string{"4200-aa"}; 261 | auto os = std::ostringstream{}; 262 | auto progress = Progress{os}; 263 | const auto result = recoverPassword({0x0d892b8b, 0x02dd8fad, 0x77f52c7b}, 264 | {l, l, l, l, l, l, l, l, {'-'}, d, d, d, d}, start, 1, false, progress); 265 | 266 | CHECK(result.size() == 1); 267 | CHECK(result[0] == "password-1234"); 268 | 269 | CHECK(start == "4400-aa"); 270 | CHECK(progress.done == 44); 271 | CHECK(progress.total == 100); 272 | CHECK(progress.state == Progress::State::EarlyExit); 273 | } 274 | 275 | TEST("exhaustive mask-based recovery") 276 | { 277 | auto start = std::string{}; 278 | auto os = std::ostringstream{}; 279 | auto progress = Progress{os}; 280 | const auto result = recoverPassword({0x0d892b8b, 0x02dd8fad, 0x77f52c7b}, 281 | {l, l, l, l, l, l, l, l, {'-'}, d, d, d, d}, start, 1, true, progress); 282 | 283 | CHECK(result.size() == 1); 284 | CHECK(result[0] == "password-1234"); 285 | 286 | CHECK(start == ""); 287 | CHECK(progress.done == 100); 288 | CHECK(progress.total == 100); 289 | CHECK(progress.state == Progress::State::Normal); 290 | } 291 | 292 | TEST("mask-based recovery attempt with solution out of given mask") 293 | { 294 | auto start = std::string{}; 295 | auto os = std::ostringstream{}; 296 | auto progress = Progress{os}; 297 | const auto result = recoverPassword({0x6cf4e702, 0x193a82f5, 0x88360a9f}, 298 | {l, l, l, l, l, l, l, l, {'-'}, d, d, d, d}, start, 1, false, progress); 299 | 300 | CHECK(result.empty()); 301 | 302 | CHECK(start == ""); 303 | CHECK(progress.done == 100); 304 | CHECK(progress.total == 100); 305 | CHECK(progress.state == Progress::State::Normal); 306 | const auto s = os.str(); 307 | CHECK(os.str().find("Password: passw*rd-1234 (as bytes: 70 61 73 73 77 2a 72 64 2d 31 32 33 34)") != 308 | std::string::npos); 309 | } 310 | 311 | TEST("mask-based recovery with constant mask") 312 | { 313 | auto start = std::string{}; 314 | auto os = std::ostringstream{}; 315 | auto progress = Progress{os}; 316 | const auto result = 317 | recoverPassword({0xc80f5189, 0xce16bd43, 0x38247eb5}, 318 | {{'L'}, {'o'}, {'r'}, {'e'}, {'m'}, {' '}, {'i'}, {'p'}, {'s'}, {'u'}, {'m'}, {' '}, {'d'}, 319 | {'o'}, {'l'}, {'o'}, {'r'}, {' '}, {'s'}, {'i'}, {'t'}, {' '}, {'a'}, {'m'}, {'e'}, {'t'}}, 320 | start, 1, false, progress); 321 | 322 | CHECK(result.size() == 1); 323 | CHECK(result[0] == "Lorem ipsum dolor sit amet"); 324 | 325 | CHECK(start == ""); 326 | CHECK(progress.done == 0); 327 | CHECK(progress.total == 0); 328 | CHECK(progress.state == Progress::State::EarlyExit); 329 | } 330 | 331 | TEST("mask-based recovery with constant prefix") 332 | { 333 | auto start = std::string{}; 334 | auto os = std::ostringstream{}; 335 | auto progress = Progress{os}; 336 | const auto result = 337 | recoverPassword({0xc80f5189, 0xce16bd43, 0x38247eb5}, 338 | {{'L'}, {'o'}, {'r'}, {'e'}, {'m'}, {' '}, {'i'}, {'p'}, {'s'}, {'u'}, {'m'}, {' '}, {'d'}, 339 | {'o'}, {'l'}, {'o'}, {'r'}, {' '}, sl, sl, sl, sl, sl, sl, sl, sl}, 340 | start, 1, false, progress); 341 | 342 | CHECK(result.size() == 1); 343 | CHECK(result[0] == "Lorem ipsum dolor sit amet"); 344 | 345 | CHECK(start == ""); 346 | CHECK(progress.done == 0); 347 | CHECK(progress.total == 0); 348 | CHECK(progress.state == Progress::State::EarlyExit); 349 | } 350 | 351 | TEST("mask-based recovery with constant suffix") 352 | { 353 | auto start = std::string{}; 354 | auto os = std::ostringstream{}; 355 | auto progress = Progress{os}; 356 | const auto result = 357 | recoverPassword({0xc80f5189, 0xce16bd43, 0x38247eb5}, 358 | {u, sl, sl, sl, sl, {' '}, {'i'}, {'p'}, {'s'}, {'u'}, {'m'}, {' '}, {'d'}, 359 | {'o'}, {'l'}, {'o'}, {'r'}, {' '}, {'s'}, {'i'}, {'t'}, {' '}, {'a'}, {'m'}, {'e'}, {'t'}}, 360 | start, 1, false, progress); 361 | 362 | CHECK(result.size() == 1); 363 | CHECK(result[0] == "Lorem ipsum dolor sit amet"); 364 | 365 | CHECK(start == ""); 366 | CHECK(progress.done == 0); 367 | CHECK(progress.total == 0); 368 | CHECK(progress.state == Progress::State::EarlyExit); 369 | } 370 | 371 | TEST("mask-based recovery with constant prefix and suffix") 372 | { 373 | 374 | auto start = std::string{}; 375 | auto os = std::ostringstream{}; 376 | auto progress = Progress{os}; 377 | const auto result = 378 | recoverPassword({0xc80f5189, 0xce16bd43, 0x38247eb5}, 379 | {{'L'}, {'o'}, {'r'}, {'e'}, {'m'}, {' '}, {'i'}, {'p'}, {'s'}, {'u'}, {'m'}, {' '}, sl, 380 | sl, sl, sl, sl, sl, sl, sl, sl, {' '}, {'a'}, {'m'}, {'e'}, {'t'}}, 381 | start, 1, false, progress); 382 | 383 | CHECK(result.size() == 1); 384 | CHECK(result[0] == "Lorem ipsum dolor sit amet"); 385 | 386 | CHECK(start == ""); 387 | CHECK(progress.done == 0); 388 | CHECK(progress.total == 0); 389 | CHECK(progress.state == Progress::State::EarlyExit); 390 | } 391 | 392 | TEST("mask-based recovery with sparse constant characters") 393 | { 394 | 395 | auto start = std::string{}; 396 | auto os = std::ostringstream{}; 397 | auto progress = Progress{os}; 398 | const auto result = 399 | recoverPassword({0xc80f5189, 0xce16bd43, 0x38247eb5}, 400 | {{'L'}, {'o'}, l, {'e'}, l, {' '}, {'i'}, l, {'s'}, l, {'m'}, {' '}, {'d'}, 401 | {'o'}, l, {'o'}, l, {' '}, {'s'}, {'i'}, {'t'}, {' '}, {'a'}, l, {'e'}, {'t'}}, 402 | start, 1, false, progress); 403 | 404 | CHECK(result.size() == 1); 405 | CHECK(result[0] == "Lorem ipsum dolor sit amet"); 406 | 407 | CHECK(start == "tena tis aoaod Loaea"); 408 | CHECK(progress.done == 0); 409 | CHECK(progress.total == 0); 410 | CHECK(progress.state == Progress::State::EarlyExit); 411 | } 412 | -------------------------------------------------------------------------------- /tests/bkcrack/Zip.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace 11 | { 12 | const auto testFolder = std::filesystem::path{__FILE__}.replace_filename("data"); 13 | } // namespace 14 | 15 | TEST("Zip::Error") 16 | { 17 | const auto error = Zip::Error{"description"}; 18 | CHECK(error.what() == std::string_view{"Zip error: description."}); 19 | } 20 | 21 | TEST("parse empty.zip") 22 | { 23 | auto ifs = std::ifstream{testFolder / "empty.zip", std::ios::binary}; 24 | CHECK(ifs.is_open()); 25 | 26 | const auto zip = Zip{ifs}; 27 | CHECK(zip.begin() == zip.end()); 28 | } 29 | 30 | TEST("fail to parse non-zip file") 31 | { 32 | auto ifs = std::ifstream{testFolder / "make_test_data.sh", std::ios::binary}; 33 | CHECK(ifs.is_open()); 34 | 35 | CHECK_THROWS(Zip::Error, "could not find end of central directory signature", Zip{ifs}); 36 | } 37 | 38 | TEST("parse plan.zip") 39 | { 40 | auto ifs = std::ifstream{testFolder / "plain.zip", std::ios::binary}; 41 | CHECK(ifs.is_open()); 42 | 43 | const auto zip = Zip{ifs}; 44 | auto it = zip.begin(); 45 | const auto end = zip.end(); 46 | 47 | CHECK(it != zip.end()); 48 | auto entry = *it; 49 | CHECK(entry.name == "store.txt"); 50 | CHECK(entry.encryption == Zip::Encryption::None); 51 | CHECK(entry.compression == Zip::Compression::Store); 52 | CHECK(entry.crc32 == 0x1ca08acd); 53 | CHECK(entry.offset == 0); 54 | CHECK(entry.packedSize == 208); 55 | CHECK(entry.uncompressedSize == 208); 56 | CHECK(entry.checkByte == 0x1c); 57 | 58 | CHECK(++it != zip.end()); 59 | entry = *it; 60 | CHECK(entry.name == "deflate.txt"); 61 | CHECK(entry.encryption == Zip::Encryption::None); 62 | CHECK(entry.compression == Zip::Compression::Deflate); 63 | CHECK(entry.crc32 == 0x45e207a8); 64 | CHECK(entry.offset == 247); 65 | CHECK(entry.packedSize == 71); 66 | CHECK(entry.uncompressedSize == 260); 67 | CHECK(entry.checkByte == 0x45); 68 | 69 | CHECK(++it == zip.end()); 70 | } 71 | 72 | TEST("parse zipcrypto.zip") 73 | { 74 | auto ifs = std::ifstream{testFolder / "zipcrypto.zip", std::ios::binary}; 75 | CHECK(ifs.is_open()); 76 | 77 | const auto zip = Zip{ifs}; 78 | auto it = zip.begin(); 79 | const auto end = zip.end(); 80 | 81 | CHECK(it != zip.end()); 82 | auto entry = *it; 83 | CHECK(entry.name == "store.txt"); 84 | CHECK(entry.encryption == Zip::Encryption::Traditional); 85 | CHECK(entry.compression == Zip::Compression::Store); 86 | CHECK(entry.crc32 == 0x1ca08acd); 87 | CHECK(entry.offset == 0); 88 | CHECK(entry.packedSize == 220); 89 | CHECK(entry.uncompressedSize == 208); 90 | CHECK(entry.checkByte == 0xab); 91 | 92 | CHECK(++it != zip.end()); 93 | entry = *it; 94 | CHECK(entry.name == "deflate.txt"); 95 | CHECK(entry.encryption == Zip::Encryption::Traditional); 96 | CHECK(entry.compression == Zip::Compression::Deflate); 97 | CHECK(entry.crc32 == 0x45e207a8); 98 | CHECK(entry.offset == 275); 99 | CHECK(entry.packedSize == 83); 100 | CHECK(entry.uncompressedSize == 260); 101 | CHECK(entry.checkByte == 0xab); 102 | 103 | CHECK(++it == zip.end()); 104 | } 105 | 106 | TEST("parse zip64.zip") 107 | { 108 | auto ifs = std::ifstream{testFolder / "zip64.zip", std::ios::binary}; 109 | CHECK(ifs.is_open()); 110 | 111 | const auto zip = Zip{ifs}; 112 | auto it = zip.begin(); 113 | const auto end = zip.end(); 114 | 115 | CHECK(it != zip.end()); 116 | auto entry = *it; 117 | CHECK(entry.name == "store.txt"); 118 | CHECK(entry.encryption == Zip::Encryption::None); 119 | CHECK(entry.compression == Zip::Compression::Store); 120 | CHECK(entry.crc32 == 0x1ca08acd); 121 | CHECK(entry.offset == 0); 122 | CHECK(entry.packedSize == 208); 123 | CHECK(entry.uncompressedSize == 208); 124 | CHECK(entry.checkByte == 0x1c); 125 | 126 | CHECK(++it != zip.end()); 127 | entry = *it; 128 | CHECK(entry.name == "deflate.txt"); 129 | CHECK(entry.encryption == Zip::Encryption::None); 130 | CHECK(entry.compression == Zip::Compression::Deflate); 131 | CHECK(entry.crc32 == 0x45e207a8); 132 | CHECK(entry.offset == 267); 133 | CHECK(entry.packedSize == 71); 134 | CHECK(entry.uncompressedSize == 260); 135 | CHECK(entry.checkByte == 0x45); 136 | 137 | CHECK(++it == zip.end()); 138 | } 139 | 140 | TEST("parse zip64-zipcrypto.zip") 141 | { 142 | auto ifs = std::ifstream{testFolder / "zip64-zipcrypto.zip", std::ios::binary}; 143 | CHECK(ifs.is_open()); 144 | 145 | const auto zip = Zip{ifs}; 146 | auto it = zip.begin(); 147 | const auto end = zip.end(); 148 | 149 | CHECK(it != zip.end()); 150 | auto entry = *it; 151 | CHECK(entry.name == "store.txt"); 152 | CHECK(entry.encryption == Zip::Encryption::Traditional); 153 | CHECK(entry.compression == Zip::Compression::Store); 154 | CHECK(entry.crc32 == 0x1ca08acd); 155 | CHECK(entry.offset == 0); 156 | CHECK(entry.packedSize == 220); 157 | CHECK(entry.uncompressedSize == 208); 158 | CHECK(entry.checkByte == 0xab); 159 | 160 | CHECK(++it != zip.end()); 161 | entry = *it; 162 | CHECK(entry.name == "deflate.txt"); 163 | CHECK(entry.encryption == Zip::Encryption::Traditional); 164 | CHECK(entry.compression == Zip::Compression::Deflate); 165 | CHECK(entry.crc32 == 0x45e207a8); 166 | CHECK(entry.offset == 303); 167 | CHECK(entry.packedSize == 83); 168 | CHECK(entry.uncompressedSize == 260); 169 | CHECK(entry.checkByte == 0xab); 170 | 171 | CHECK(++it == zip.end()); 172 | } 173 | 174 | TEST("parse aes256.zip") 175 | { 176 | auto ifs = std::ifstream{testFolder / "aes256.zip", std::ios::binary}; 177 | CHECK(ifs.is_open()); 178 | 179 | const auto zip = Zip{ifs}; 180 | auto it = zip.begin(); 181 | const auto end = zip.end(); 182 | 183 | CHECK(it != zip.end()); 184 | auto entry = *it; 185 | CHECK(entry.name == "deflate.txt"); 186 | CHECK(entry.encryption == Zip::Encryption::Unsupported); 187 | CHECK(entry.compression == Zip::Compression::Deflate); 188 | CHECK(entry.crc32 == 0x00000000); 189 | CHECK(entry.offset == 0); 190 | CHECK(entry.packedSize == 95); 191 | CHECK(entry.uncompressedSize == 260); 192 | CHECK(entry.checkByte == 0x00); 193 | 194 | CHECK(++it != zip.end()); 195 | entry = *it; 196 | CHECK(entry.name == "store.txt"); 197 | CHECK(entry.encryption == Zip::Encryption::Unsupported); 198 | CHECK(entry.compression == Zip::Compression::Store); 199 | CHECK(entry.crc32 == 0x00000000); 200 | CHECK(entry.offset == 147); 201 | CHECK(entry.packedSize == 236); 202 | CHECK(entry.uncompressedSize == 208); 203 | CHECK(entry.checkByte == 0x00); 204 | 205 | CHECK(++it == end); 206 | } 207 | 208 | TEST("get entry by name") 209 | { 210 | auto ifs = std::ifstream{testFolder / "plain.zip", std::ios::binary}; 211 | CHECK(ifs.is_open()); 212 | 213 | const auto zip = Zip{ifs}; 214 | 215 | const auto entryStore = zip["store.txt"]; 216 | CHECK(entryStore.name == "store.txt"); 217 | 218 | const auto entryDeflate = zip["deflate.txt"]; 219 | CHECK(entryDeflate.name == "deflate.txt"); 220 | 221 | CHECK_THROWS(Zip::Error, "Zip error: found no entry named \"does not exist\".", zip["does not exist"]); 222 | } 223 | 224 | TEST("get entry by index") 225 | { 226 | auto ifs = std::ifstream{testFolder / "plain.zip", std::ios::binary}; 227 | CHECK(ifs.is_open()); 228 | 229 | const auto zip = Zip{ifs}; 230 | 231 | const auto entry0 = zip[0]; 232 | CHECK(entry0.name == "store.txt"); 233 | 234 | const auto entry1 = zip[1]; 235 | CHECK(entry1.name == "deflate.txt"); 236 | 237 | CHECK_THROWS(Zip::Error, "Zip error: found no entry at index 2 (maximum index for this archive is 1).", zip[2]); 238 | } 239 | 240 | TEST("checkEncryption") 241 | { 242 | auto entry = Zip::Entry{}; 243 | entry.name = "test"; 244 | 245 | entry.encryption = Zip::Encryption::None; 246 | Zip::checkEncryption(entry, Zip::Encryption::None); 247 | CHECK_THROWS(Zip::Error, "Zip error: entry \"test\" is not encrypted.", 248 | Zip::checkEncryption(entry, Zip::Encryption::Traditional)); 249 | 250 | entry.encryption = Zip::Encryption::Traditional; 251 | CHECK_THROWS(Zip::Error, "Zip error: entry \"test\" is encrypted.", 252 | Zip::checkEncryption(entry, Zip::Encryption::None)); 253 | Zip::checkEncryption(entry, Zip::Encryption::Traditional); 254 | 255 | entry.encryption = Zip::Encryption::Unsupported; 256 | CHECK_THROWS(Zip::Error, "Zip error: entry \"test\" is encrypted.", 257 | Zip::checkEncryption(entry, Zip::Encryption::None)); 258 | CHECK_THROWS(Zip::Error, "Zip error: entry \"test\" is encrypted with an unsupported algorithm.", 259 | Zip::checkEncryption(entry, Zip::Encryption::Traditional)); 260 | } 261 | 262 | TEST("seek to entry's data") 263 | { 264 | auto ifs = std::ifstream{testFolder / "plain.zip", std::ios::binary}; 265 | CHECK(ifs.is_open()); 266 | 267 | const auto zip = Zip{ifs}; 268 | const auto entry = zip["store.txt"]; 269 | zip.seek(entry); 270 | CHECK(ifs.tellg() == 39); 271 | CHECK(ifs.get() == 's'); 272 | CHECK(ifs.get() == 't'); 273 | CHECK(ifs.get() == 'o'); 274 | CHECK(ifs.get() == 'r'); 275 | CHECK(ifs.get() == 'e'); 276 | CHECK(ifs.get() == ' '); 277 | CHECK(ifs.get() == 'A'); 278 | } 279 | 280 | TEST("load entry's data") 281 | { 282 | auto ifs = std::ifstream{testFolder / "plain.zip", std::ios::binary}; 283 | CHECK(ifs.is_open()); 284 | 285 | const auto zip = Zip{ifs}; 286 | const auto entry = zip["store.txt"]; 287 | 288 | const auto data = zip.load(entry); 289 | CHECK(data.size() == 208); 290 | CHECK(data.front() == 's'); 291 | CHECK(data.back() == '\n'); 292 | 293 | const auto data5 = zip.load(entry, 7); 294 | CHECK(data5 == std::vector{'s', 't', 'o', 'r', 'e', ' ', 'A'}); 295 | } 296 | 297 | TEST("changeKeys") 298 | { 299 | auto ifs = std::ifstream{testFolder / "zipcrypto.zip", std::ios::binary}; 300 | CHECK(ifs.is_open()); 301 | 302 | auto progressOutput = std::ostringstream{}; 303 | auto progress = Progress{progressOutput}; 304 | auto newZipStream = std::stringstream{std::ios::in | std::ios::out | std::ios::binary}; 305 | const auto zip = Zip{ifs}; 306 | zip.changeKeys(newZipStream, Keys{"password"}, Keys{"new_password"}, progress); 307 | CHECK(progress.done == 2); 308 | CHECK(progress.total == 2); 309 | CHECK(progressOutput.str().empty()); 310 | 311 | const auto newZip = Zip{newZipStream}; 312 | const auto newStore = newZip["store.txt"]; 313 | const auto newDeflate = newZip["deflate.txt"]; 314 | 315 | CHECK(newStore.name == "store.txt"); 316 | CHECK(newStore.encryption == Zip::Encryption::Traditional); 317 | CHECK(newStore.compression == Zip::Compression::Store); 318 | CHECK(newStore.crc32 == 0x1ca08acd); 319 | CHECK(newStore.offset == 0); 320 | CHECK(newStore.packedSize == 220); 321 | CHECK(newStore.uncompressedSize == 208); 322 | CHECK(newStore.checkByte == 0xab); 323 | 324 | CHECK(newDeflate.name == "deflate.txt"); 325 | CHECK(newDeflate.encryption == Zip::Encryption::Traditional); 326 | CHECK(newDeflate.compression == Zip::Compression::Deflate); 327 | CHECK(newDeflate.crc32 == 0x45e207a8); 328 | CHECK(newDeflate.offset == 275); 329 | CHECK(newDeflate.packedSize == 83); 330 | CHECK(newDeflate.uncompressedSize == 260); 331 | CHECK(newDeflate.checkByte == 0xab); 332 | 333 | newZip.seek(newStore); 334 | auto deciphered = std::ostringstream{std::ios::binary}; 335 | decipher(newZipStream, 12 + 7, 11, deciphered, Keys{"new_password"}); 336 | CHECK(deciphered.str() == "\xabstore A"); 337 | } 338 | 339 | TEST("decrypt zipcrypto.zip") 340 | { 341 | auto ifs = std::ifstream{testFolder / "zipcrypto.zip", std::ios::binary}; 342 | CHECK(ifs.is_open()); 343 | 344 | auto progressOutput = std::ostringstream{}; 345 | auto progress = Progress{progressOutput}; 346 | auto newZipStream = std::stringstream{std::ios::in | std::ios::out | std::ios::binary}; 347 | const auto zip = Zip{ifs}; 348 | zip.decrypt(newZipStream, Keys{"password"}, progress); 349 | CHECK(progress.done == 2); 350 | CHECK(progress.total == 2); 351 | CHECK(progressOutput.str().empty()); 352 | 353 | const auto newZip = Zip{newZipStream}; 354 | const auto newStore = newZip["store.txt"]; 355 | const auto newDeflate = newZip["deflate.txt"]; 356 | 357 | CHECK(newStore.name == "store.txt"); 358 | CHECK(newStore.encryption == Zip::Encryption::None); 359 | CHECK(newStore.compression == Zip::Compression::Store); 360 | CHECK(newStore.crc32 == 0x1ca08acd); 361 | CHECK(newStore.offset == 0); 362 | CHECK(newStore.packedSize == 208); 363 | CHECK(newStore.uncompressedSize == 208); 364 | CHECK(newStore.checkByte == 0xab); 365 | 366 | CHECK(newDeflate.name == "deflate.txt"); 367 | CHECK(newDeflate.encryption == Zip::Encryption::None); 368 | CHECK(newDeflate.compression == Zip::Compression::Deflate); 369 | CHECK(newDeflate.crc32 == 0x45e207a8); 370 | CHECK(newDeflate.offset == 263); 371 | CHECK(newDeflate.packedSize == 71); 372 | CHECK(newDeflate.uncompressedSize == 260); 373 | CHECK(newDeflate.checkByte == 0xab); 374 | 375 | CHECK(newZip.load(newStore, 7) == std::vector{'s', 't', 'o', 'r', 'e', ' ', 'A'}); 376 | } 377 | 378 | TEST("decrypt zip64-zipcrypto.zip") 379 | { 380 | auto ifs = std::ifstream{testFolder / "zip64-zipcrypto.zip", std::ios::binary}; 381 | CHECK(ifs.is_open()); 382 | 383 | auto progressOutput = std::ostringstream{}; 384 | auto progress = Progress{progressOutput}; 385 | auto newZipStream = std::stringstream{std::ios::in | std::ios::out | std::ios::binary}; 386 | const auto zip = Zip{ifs}; 387 | zip.decrypt(newZipStream, Keys{"password"}, progress); 388 | CHECK(progress.done == 2); 389 | CHECK(progress.total == 2); 390 | CHECK(progressOutput.str().empty()); 391 | 392 | const auto newZip = Zip{newZipStream}; 393 | const auto newStore = newZip["store.txt"]; 394 | const auto newDeflate = newZip["deflate.txt"]; 395 | 396 | CHECK(newStore.name == "store.txt"); 397 | CHECK(newStore.encryption == Zip::Encryption::None); 398 | CHECK(newStore.compression == Zip::Compression::Store); 399 | CHECK(newStore.crc32 == 0x1ca08acd); 400 | CHECK(newStore.offset == 0); 401 | CHECK(newStore.packedSize == 208); 402 | CHECK(newStore.uncompressedSize == 208); 403 | CHECK(newStore.checkByte == 0xab); 404 | 405 | CHECK(newDeflate.name == "deflate.txt"); 406 | CHECK(newDeflate.encryption == Zip::Encryption::None); 407 | CHECK(newDeflate.compression == Zip::Compression::Deflate); 408 | CHECK(newDeflate.crc32 == 0x45e207a8); 409 | CHECK(newDeflate.offset == 291); 410 | CHECK(newDeflate.packedSize == 71); 411 | CHECK(newDeflate.uncompressedSize == 260); 412 | CHECK(newDeflate.checkByte == 0xab); 413 | 414 | CHECK(newZip.load(newStore, 7) == std::vector{'s', 't', 'o', 'r', 'e', ' ', 'A'}); 415 | } 416 | 417 | TEST("decrypt aes256.zip does nothing") 418 | { 419 | auto ifs = std::ifstream{testFolder / "aes256.zip", std::ios::binary}; 420 | CHECK(ifs.is_open()); 421 | 422 | auto progressOutput = std::ostringstream{}; 423 | auto progress = Progress{progressOutput}; 424 | auto newZipStream = std::stringstream{std::ios::in | std::ios::out | std::ios::binary}; 425 | const auto zip = Zip{ifs}; 426 | zip.decrypt(newZipStream, Keys{"password"}, progress); 427 | CHECK(progress.done == 0); 428 | CHECK(progress.total == 0); 429 | CHECK(progressOutput.str().empty()); 430 | 431 | ifs.seekg(0, std::ios::beg); 432 | CHECK(std::equal(std::istreambuf_iterator{ifs}, std::istreambuf_iterator{}, 433 | std::istreambuf_iterator{newZipStream}, std::istreambuf_iterator{})); 434 | } 435 | 436 | TEST("decipher") 437 | { 438 | auto ifs = std::ifstream{testFolder / "zipcrypto.zip", std::ios::binary}; 439 | CHECK(ifs.is_open()); 440 | 441 | const auto zip = Zip{ifs}; 442 | zip.seek(zip["store.txt"]); 443 | auto oss = std::ostringstream{std::ios::binary}; 444 | decipher(ifs, 12 + 23, 12, oss, Keys{"password"}); 445 | 446 | CHECK(oss.str() == "store A store B store C"); 447 | } 448 | -------------------------------------------------------------------------------- /tests/cli/Arguments.test.cpp: -------------------------------------------------------------------------------- 1 | #include "Arguments.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | TEST("Arguments::Error") 8 | { 9 | const auto error = Arguments::Error{"description"}; 10 | CHECK(error.what() == std::string_view{"Arguments error: description."}); 11 | } 12 | 13 | TEST("attack with files") 14 | { 15 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain"}; 16 | const auto args = Arguments{argv.size(), argv.data()}; 17 | 18 | CHECK(args.cipherFile == "cipher"); 19 | CHECK(args.plainFile == "plain"); 20 | 21 | CHECK(args.plainFilePrefix == 1024 * 1024); 22 | CHECK(args.offset == 0); 23 | CHECK(args.extraPlaintext.empty()); 24 | CHECK(args.ignoreCheckByte == false); 25 | CHECK(args.attackStart == 0); 26 | CHECK(args.jobs >= 1); 27 | CHECK(args.exhaustive == false); 28 | } 29 | 30 | TEST("attack with entry names") 31 | { 32 | const auto argv = std::array{"bkcrack", "-C", "encrypted.zip", "-c", "cipher", "-P", "plain.zip", "-p", "plain"}; 33 | const auto args = Arguments{argv.size(), argv.data()}; 34 | 35 | CHECK(args.cipherArchive == "encrypted.zip"); 36 | CHECK(args.cipherFile == "cipher"); 37 | CHECK(args.plainArchive == "plain.zip"); 38 | CHECK(args.plainFile == "plain"); 39 | } 40 | 41 | TEST("attack with entry indices") 42 | { 43 | const auto argv = std::array{ 44 | "bkcrack", "-C", "encrypted.zip", "--cipher-index", "1", "-P", "plain.zip", "--plain-index", "2", 45 | }; 46 | const auto args = Arguments{argv.size(), argv.data()}; 47 | 48 | CHECK(args.cipherArchive == "encrypted.zip"); 49 | CHECK(args.cipherIndex == 1); 50 | CHECK(args.plainArchive == "plain.zip"); 51 | CHECK(args.plainIndex == 2); 52 | } 53 | 54 | TEST("truncate plaintext") 55 | { 56 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-t", "42"}; 57 | const auto args = Arguments{argv.size(), argv.data()}; 58 | 59 | CHECK(args.plainFilePrefix == 42); 60 | } 61 | 62 | TEST("plaintext offset") 63 | { 64 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-o", "123"}; 65 | const auto args = Arguments{argv.size(), argv.data()}; 66 | 67 | CHECK(args.offset == 123); 68 | } 69 | 70 | TEST("extra plaintext") 71 | { 72 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-x", "10", "012345", "-x", "20", "6789ab"}; 73 | const auto args = Arguments{argv.size(), argv.data()}; 74 | 75 | CHECK(args.extraPlaintext == 76 | std::map{{10, 0x01}, {11, 0x23}, {12, 0x45}, {20, 0x67}, {21, 0x89}, {22, 0xab}}); 77 | } 78 | 79 | TEST("ignore check byte") 80 | { 81 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "--ignore-check-byte"}; 82 | const auto args = Arguments{argv.size(), argv.data()}; 83 | 84 | CHECK(args.ignoreCheckByte); 85 | } 86 | 87 | TEST("attack checkpoint") 88 | { 89 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "--continue-attack", "456"}; 90 | const auto args = Arguments{argv.size(), argv.data()}; 91 | 92 | CHECK(args.attackStart == 456); 93 | } 94 | 95 | TEST("attack thread count") 96 | { 97 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-j", "7"}; 98 | const auto args = Arguments{argv.size(), argv.data()}; 99 | 100 | CHECK(args.jobs == 7); 101 | } 102 | 103 | TEST("exhaustive") 104 | { 105 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-e"}; 106 | const auto args = Arguments{argv.size(), argv.data()}; 107 | 108 | CHECK(args.exhaustive == true); 109 | } 110 | 111 | TEST("password") 112 | { 113 | const auto argv = std::array{"bkcrack", "--password", "password"}; 114 | const auto args = Arguments{argv.size(), argv.data()}; 115 | 116 | CHECK(args.password == "password"); 117 | } 118 | 119 | TEST("decipher") 120 | { 121 | const auto argv = std::array{ 122 | "bkcrack", "-k", "ab", "cd", "ef", "-c", "cipher", "-d", "output", 123 | }; 124 | const auto args = Arguments{argv.size(), argv.data()}; 125 | 126 | CHECK(args.keys.has_value()); 127 | CHECK(args.keys->getX() == 0xab); 128 | CHECK(args.keys->getY() == 0xcd); 129 | CHECK(args.keys->getZ() == 0xef); 130 | CHECK(args.cipherFile == "cipher"); 131 | CHECK(args.decipheredFile == "output"); 132 | CHECK(args.keepHeader == false); 133 | } 134 | 135 | TEST("decipher with header") 136 | { 137 | const auto argv = std::array{ 138 | "bkcrack", "-k", "ab", "cd", "ef", "-c", "cipher", "-d", "output", "--keep-header", 139 | }; 140 | const auto args = Arguments{argv.size(), argv.data()}; 141 | 142 | CHECK(args.keys.has_value()); 143 | CHECK(args.cipherFile == "cipher"); 144 | CHECK(args.decipheredFile == "output"); 145 | CHECK(args.keepHeader == true); 146 | } 147 | 148 | TEST("decrypt") 149 | { 150 | const auto argv = std::array{ 151 | "bkcrack", "-k", "ab", "cd", "ef", "-C", "encrypted.zip", "-D", "output.zip", 152 | }; 153 | const auto args = Arguments{argv.size(), argv.data()}; 154 | 155 | CHECK(args.keys.has_value()); 156 | CHECK(args.cipherArchive == "encrypted.zip"); 157 | CHECK(args.decryptedArchive == "output.zip"); 158 | } 159 | 160 | TEST("change password") 161 | { 162 | const auto argv = std::array{ 163 | "bkcrack", "-k", "ab", "cd", "ef", "-C", "encrypted.zip", "-U", "output.zip", "password", 164 | }; 165 | const auto args = Arguments{argv.size(), argv.data()}; 166 | 167 | CHECK(args.keys.has_value()); 168 | CHECK(args.cipherArchive == "encrypted.zip"); 169 | CHECK(args.changePassword.has_value()); 170 | CHECK(args.changePassword->unlockedArchive == "output.zip"); 171 | CHECK(args.changePassword->newPassword == "password"); 172 | } 173 | 174 | TEST("change keys") 175 | { 176 | const auto argv = std::array{ 177 | "bkcrack", "-k", "ab", "cd", "ef", "-C", "encrypted.zip", "--change-keys", "output.zip", "123", "456", "789", 178 | }; 179 | const auto args = Arguments{argv.size(), argv.data()}; 180 | 181 | CHECK(args.keys.has_value()); 182 | CHECK(args.cipherArchive == "encrypted.zip"); 183 | CHECK(args.changeKeys.has_value()); 184 | CHECK(args.changeKeys->unlockedArchive == "output.zip"); 185 | CHECK(args.changeKeys->newKeys.getX() == 0x123); 186 | CHECK(args.changeKeys->newKeys.getY() == 0x456); 187 | CHECK(args.changeKeys->newKeys.getZ() == 0x789); 188 | } 189 | 190 | TEST("bruteforce") 191 | { 192 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-b", "?d"}; 193 | const auto args = Arguments{argv.size(), argv.data()}; 194 | 195 | CHECK(args.keys.has_value()); 196 | CHECK(args.bruteforce == std::vector{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}); 197 | CHECK(args.length == std::nullopt); 198 | } 199 | 200 | TEST("bruteforce with length range") 201 | { 202 | for (const auto& [arg, min, max] : { 203 | std::tuple{"5..15", 5ul, std::size_t{15}}, 204 | std::tuple{"..15", 0ul, std::size_t{15}}, 205 | std::tuple{"5..", 5ul, std::numeric_limits::max()}, 206 | std::tuple{"8", 8ul, std::size_t{8}}, 207 | }) 208 | { 209 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-b", "?d", "-l", arg}; 210 | const auto args = Arguments{argv.size(), argv.data()}; 211 | 212 | CHECK(args.keys.has_value()); 213 | CHECK(args.bruteforce.has_value()); 214 | CHECK(args.length.has_value()); 215 | CHECK(args.length->minLength == min); 216 | CHECK(args.length->maxLength == max); 217 | } 218 | } 219 | 220 | TEST("bruteforce charset and length") 221 | { 222 | for (const auto& [arg, min, max] : { 223 | std::tuple{"5..15", 5ul, std::size_t{15}}, 224 | std::tuple{"..15", 0ul, std::size_t{15}}, 225 | std::tuple{"5..", 5ul, std::numeric_limits::max()}, 226 | std::tuple{"8", 0ul, std::size_t{8}}, 227 | }) 228 | { 229 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-r", arg, "?d"}; 230 | const auto args = Arguments{argv.size(), argv.data()}; 231 | 232 | CHECK(args.keys.has_value()); 233 | CHECK(args.bruteforce.has_value()); 234 | CHECK(args.length.has_value()); 235 | CHECK(args.length->minLength == min); 236 | CHECK(args.length->maxLength == max); 237 | } 238 | } 239 | 240 | TEST("mask") 241 | { 242 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-m", "?u?l?d??0"}; 243 | const auto args = Arguments{argv.size(), argv.data()}; 244 | 245 | const auto expected = std::vector>{ 246 | {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 247 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}, 248 | {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 249 | 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}, 250 | {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}, 251 | {'?'}, 252 | {'0'}, 253 | }; 254 | 255 | CHECK(args.keys.has_value()); 256 | CHECK(args.mask == expected); 257 | } 258 | 259 | TEST("mask with custom charsets") 260 | { 261 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-m", "?x?y", "-s", "x", "123", "-s", "y", "456?x"}; 262 | const auto args = Arguments{argv.size(), argv.data()}; 263 | 264 | CHECK(args.keys.has_value()); 265 | CHECK(args.mask == std::vector>{{'1', '2', '3'}, {'1', '2', '3', '4', '5', '6'}}); 266 | } 267 | 268 | TEST("password recovery checkpoint") 269 | { 270 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-b", "?d", "--continue-recovery", "343536"}; 271 | const auto args = Arguments{argv.size(), argv.data()}; 272 | 273 | CHECK(args.keys.has_value()); 274 | CHECK(args.bruteforce.has_value()); 275 | CHECK(args.recoveryStart == "456"); 276 | } 277 | 278 | TEST("list") 279 | { 280 | const auto argv = std::array{"bkcrack", "-L", "archive.zip"}; 281 | const auto args = Arguments{argv.size(), argv.data()}; 282 | CHECK(args.infoArchive == "archive.zip"); 283 | } 284 | 285 | TEST("version") 286 | { 287 | const auto argv = std::array{"bkcrack", "--version"}; 288 | const auto args = Arguments{argv.size(), argv.data()}; 289 | CHECK(args.version); 290 | } 291 | 292 | TEST("help") 293 | { 294 | for (const auto& argv : {std::array{"bkcrack", "-h"}, std::array{"bkcrack", "--help"}}) 295 | { 296 | const auto args = Arguments{static_cast(argv.size()), argv.data()}; 297 | CHECK(args.help); 298 | } 299 | } 300 | 301 | TEST("no arguments") 302 | { 303 | const auto argv = std::array{"bkcrack"}; 304 | CHECK_THROWS(Arguments::Error, "Arguments error", Arguments{argv.size(), argv.data()}); 305 | } 306 | 307 | TEST("invalid number") 308 | { 309 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-o", "text"}; 310 | CHECK_THROWS(Arguments::Error, "expected an integer, got \"text\"", Arguments{argv.size(), argv.data()}); 311 | } 312 | 313 | TEST("out of range number") 314 | { 315 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-o", "5000000000"}; 316 | CHECK_THROWS(Arguments::Error, "integer value 5000000000 is out of range", Arguments{argv.size(), argv.data()}); 317 | } 318 | 319 | TEST("missing action with -k") 320 | { 321 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef"}; 322 | CHECK_THROWS(Arguments::Error, "parameter is missing (required by -k)", Arguments{argv.size(), argv.data()}); 323 | } 324 | 325 | TEST("incompatible ciphertext entry specifications") 326 | { 327 | const auto argv = std::array{"bkcrack", "-c", "cipher", "--cipher-index", "1", "-p", "plain"}; 328 | CHECK_THROWS(Arguments::Error, "-c and --cipher-index cannot be used at the same time", 329 | Arguments{argv.size(), argv.data()}); 330 | } 331 | 332 | TEST("incompatible plaintext entry specifications") 333 | { 334 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "--plain-index", "1"}; 335 | CHECK_THROWS(Arguments::Error, "-p and --plain-index cannot be used at the same time", 336 | Arguments{argv.size(), argv.data()}); 337 | } 338 | 339 | TEST("missing ciphertext") 340 | { 341 | const auto argv = std::array{"bkcrack", "-p", "plain"}; 342 | CHECK_THROWS(Arguments::Error, "-c or --cipher-index parameter is missing", Arguments{argv.size(), argv.data()}); 343 | } 344 | 345 | TEST("missing plaintext") 346 | { 347 | const auto argv = std::array{"bkcrack", "-c", "cipher"}; 348 | CHECK_THROWS(Arguments::Error, "-p, --plain-index or -x parameter is missing", Arguments{argv.size(), argv.data()}); 349 | } 350 | 351 | TEST("missing plaintext entry") 352 | { 353 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-P", "plain.zip", "-x", "0", "abcd"}; 354 | CHECK_THROWS(Arguments::Error, "-p or --plain-index parameter is missing (required by -P)", 355 | Arguments{argv.size(), argv.data()}); 356 | } 357 | 358 | TEST("missing archive to load ciphertext entry") 359 | { 360 | const auto argv = std::array{"bkcrack", "--cipher-index", "1", "-p", "plain"}; 361 | CHECK_THROWS(Arguments::Error, "-C parameter is missing (required by --cipher-index)", 362 | Arguments{argv.size(), argv.data()}); 363 | } 364 | 365 | TEST("missing archive to load plaintext entry") 366 | { 367 | const auto argv = std::array{"bkcrack", "-c", "cipher", "--plain-index", "1"}; 368 | CHECK_THROWS(Arguments::Error, "-P parameter is missing (required by --plain-index)", 369 | Arguments{argv.size(), argv.data()}); 370 | } 371 | 372 | TEST("invalid offset") 373 | { 374 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-p", "plain", "-o", "-13"}; 375 | CHECK_THROWS(Arguments::Error, "plaintext offset -13 is too small (minimum is -12)", 376 | Arguments{argv.size(), argv.data()}); 377 | } 378 | 379 | TEST("invalid extra plaintext offset") 380 | { 381 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-x", "-13", "abcd"}; 382 | CHECK_THROWS(Arguments::Error, "extra plaintext offset -13 is too small (minimum is -12)", 383 | Arguments{argv.size(), argv.data()}); 384 | } 385 | 386 | TEST("invalid hex string length") 387 | { 388 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-x", "0", "123"}; 389 | CHECK_THROWS(Arguments::Error, "expected an even-length string, got 123", Arguments{argv.size(), argv.data()}); 390 | } 391 | 392 | TEST("invalid hex string characters") 393 | { 394 | const auto argv = std::array{"bkcrack", "-c", "cipher", "-x", "0", "ghij"}; 395 | CHECK_THROWS(Arguments::Error, "expected data in hexadecimal, got ghij", Arguments{argv.size(), argv.data()}); 396 | } 397 | 398 | TEST("invalid key component length") 399 | { 400 | const auto argv = std::array{"bkcrack", "-k", "123456789", "222", "333", "-c", "cipher", "-d", "output"}; 401 | CHECK_THROWS(Arguments::Error, "expected a string of length 8 or less, got 123456789", 402 | Arguments{argv.size(), argv.data()}); 403 | } 404 | 405 | TEST("invalid key component character") 406 | { 407 | const auto argv = std::array{"bkcrack", "-k", "ggg", "222", "333", "-c", "cipher", "-d", "output"}; 408 | CHECK_THROWS(Arguments::Error, "expected X in hexadecimal, got ggg", Arguments{argv.size(), argv.data()}); 409 | } 410 | 411 | TEST("missing ciphertext to decipher") 412 | { 413 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-d", "output"}; 414 | CHECK_THROWS(Arguments::Error, "-c or --cipher-index parameter is missing (required by -d)", 415 | Arguments{argv.size(), argv.data()}); 416 | } 417 | 418 | TEST("missing archive to decrypt") 419 | { 420 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-D", "output.zip"}; 421 | CHECK_THROWS(Arguments::Error, "-C parameter is missing (required by -D)", Arguments{argv.size(), argv.data()}); 422 | } 423 | 424 | TEST("missing archive to change password") 425 | { 426 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-U", "output.zip", "password"}; 427 | CHECK_THROWS(Arguments::Error, "-C parameter is missing (required by -U)", Arguments{argv.size(), argv.data()}); 428 | } 429 | 430 | TEST("missing archive to change keys") 431 | { 432 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "--change-keys", "output.zip", "123", "456", "789"}; 433 | CHECK_THROWS(Arguments::Error, "-C parameter is missing (required by --change-keys)", 434 | Arguments{argv.size(), argv.data()}); 435 | } 436 | 437 | TEST("missing charset") 438 | { 439 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-c", "cipher", "-d", "output", "-l", "12"}; 440 | CHECK_THROWS(Arguments::Error, "--bruteforce parameter is missing (required by --length)", 441 | Arguments{argv.size(), argv.data()}); 442 | } 443 | 444 | TEST("incompatible password recovery methods") 445 | { 446 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-b", "?a", "-m", "?a?a?a?a"}; 447 | CHECK_THROWS(Arguments::Error, "--bruteforce and --mask cannot be used at the same time", 448 | Arguments{argv.size(), argv.data()}); 449 | } 450 | 451 | TEST("unknown charset") 452 | { 453 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-b", "?x"}; 454 | CHECK_THROWS(Arguments::Error, "unknown charset ?x", Arguments{argv.size(), argv.data()}); 455 | } 456 | 457 | TEST("circular charset reference") 458 | { 459 | const auto argv = std::array{"bkcrack", "-k", "ab", "cd", "ef", "-b", "?x", "-s", "x", "?y", "-s", "y", "?x"}; 460 | CHECK_THROWS(Arguments::Error, "circular reference resolving charset ?x", Arguments{argv.size(), argv.data()}); 461 | } 462 | --------------------------------------------------------------------------------