diff --git a/CMakeLists.txt b/CMakeLists.txt index adb1c740a..712c3db42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ endif() add_subdirectory(math) add_subdirectory(others) add_subdirectory(search) +add_subdirectory(ciphers) add_subdirectory(strings) add_subdirectory(sorting) add_subdirectory(geometry) diff --git a/DIRECTORY.md b/DIRECTORY.md index d678276bc..f53d2f7ab 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -10,6 +10,9 @@ * [Rat Maze](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/backtracking/rat_maze.cpp) * [Sudoku Solve](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/backtracking/sudoku_solve.cpp) +## Ciphers + * [Hill Cipher](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/ciphers/hill_cipher.cpp) + ## Data Structures * [Avltree](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/data_structures/avltree.cpp) * [Binary Search Tree](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/data_structures/binary_search_tree.cpp) diff --git a/ciphers/CMakeLists.txt b/ciphers/CMakeLists.txt new file mode 100644 index 000000000..1efde3087 --- /dev/null +++ b/ciphers/CMakeLists.txt @@ -0,0 +1,18 @@ +# If necessary, use the RELATIVE flag, otherwise each source file may be listed +# with full pathname. RELATIVE may makes it easier to extract an executable name +# automatically. +file( GLOB APP_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.cpp ) +# file( GLOB APP_SOURCES ${CMAKE_SOURCE_DIR}/*.c ) +# AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR} APP_SOURCES) +foreach( testsourcefile ${APP_SOURCES} ) + # I used a simple string replace, to cut off .cpp. + string( REPLACE ".cpp" "" testname ${testsourcefile} ) + add_executable( ${testname} ${testsourcefile} ) + + set_target_properties(${testname} PROPERTIES LINKER_LANGUAGE CXX) + if(OpenMP_CXX_FOUND) + target_link_libraries(${testname} OpenMP::OpenMP_CXX) + endif() + install(TARGETS ${testname} DESTINATION "bin/ciphers") + +endforeach( testsourcefile ${APP_SOURCES} ) diff --git a/ciphers/hill_cipher.cpp b/ciphers/hill_cipher.cpp new file mode 100644 index 000000000..1fef0f32e --- /dev/null +++ b/ciphers/hill_cipher.cpp @@ -0,0 +1,543 @@ +/** + * @file hill_cipher.cpp + * @brief Implementation of [Hill + * cipher](https://en.wikipedia.org/wiki/Hill_cipher) algorithm. + * + * Program to generate the encryption-decryption key and perform encryption and + * decryption of ASCII text using the famous block cipher algorithm. This is a + * powerful encryption algorithm that is relatively easy to implement with a + * given key. The strength of the algorithm depends on the size of the block + * encryption matrix key; the bigger the matrix, the stronger the encryption and + * more difficult to break it. However, the important requirement for the matrix + * is that: + * 1. matrix should be invertible - all inversion conditions should be satisfied + * and + * 2. its determinant must not have any common factors with the length of + * character set + * Due to this restriction, most implementations only implement with small 3x3 + * encryption keys and a small subset of ASCII alphabets. + * + * In the current implementation, I present to you an implementation for + * generating larger encryption keys (I have attempted upto 10x10) and an ASCII + * character set of 97 printable characters. Hence, a typical ASCII text file + * could be easily encrypted with the module. The larger character set increases + * the modulo of cipher and hence the matrix determinants can get very large + * very quickly rendering them ill-defined. + * + * \note This program uses determinant computation using LU decomposition from + * the file lu_decomposition.h + * \note The matrix generation algorithm is very rudimentary and does not + * guarantee an invertible modulus matrix. \todo Better matrix generation + * algorithm. + * + * @author [Krishna Vedala](https://github.com/kvedala) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _OPENMP +#include +#endif + +#include "../numerical_methods/lu_decomposition.h" + +/** + * operator to print a matrix + */ +template +static std::ostream &operator<<(std::ostream &out, matrix const &v) { + const int width = 15; + const char separator = ' '; + + for (size_t row = 0; row < v.size(); row++) { + for (size_t col = 0; col < v[row].size(); col++) + out << std::left << std::setw(width) << std::setfill(separator) + << v[row][col]; + out << std::endl; + } + + return out; +} + +/** \namespace ciphers + * \brief Algorithms for encryption and decryption + */ +namespace ciphers { +/** dictionary of characters that can be encrypted and decrypted */ +static const char *STRKEY = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&" + "*()_+`-=[]{}|;':\",./<>?\\\r\n \0"; + +/** + * @brief Implementation of [Hill + * Cipher](https://en.wikipedia.org/wiki/Hill_cipher) algorithm + */ +class HillCipher { + private: + /** + * @brief Function to generate a random integer in a given interval + * + * @param a lower limit of interval + * @param b upper limit of interval + * @tparam T type of output + * @return random integer in the interval \f$[a,b)\f$ + */ + template + static const T2 rand_range(T1 a, T1 b) { + // generate random number between 0 and 1 + long double r = static_cast(std::rand()) / RAND_MAX; + + // scale and return random number as integer + return static_cast(r * (b - a) + a); + } + + /** + * @brief Function overload to fill a matrix with random integers in a given + * interval + * + * @param M pointer to matrix to be filled with random numbers + * @param a lower limit of interval + * @param b upper limit of interval + * @tparam T1 type of input range + * @tparam T2 type of matrix + * @return determinant of generated random matrix + * + * @warning There will need to be a balance between the matrix size and the + * range of random numbers. If the matrix is large, the range of random + * numbers must be small to have a well defined keys. Or if the matrix is + * smaller, the random numbers range can be larger. For an 8x8 matrix, range + * should be no more than \f$[0,10]\f$ + */ + template + static double rand_range(matrix *M, T1 a, T1 b) { + for (size_t i = 0; i < M->size(); i++) { + for (size_t j = 0; j < M[0][0].size(); j++) { + M[0][i][j] = rand_range(a, b); + } + } + + return determinant_lu(*M); + } + + /** + * @brief Compute + * [GCD](https://en.wikipedia.org/wiki/Greatest_common_divisor) of two + * integers using Euler's algorithm + * + * @param a first number + * @param b second number + * @return GCD of \f$a\f$ and \f$b\f$ + */ + template + static const T gcd(T a, T b) { + if (b > a) // ensure always a < b + std::swap(a, b); + + while (b != 0) { + T tmp = b; + b = a % b; + a = tmp; + } + + return a; + } + + /** + * @brief helper function to perform vector multiplication with encryption + * or decryption matrix + * + * @param vector vector to multiply + * @param key encryption or decryption key matrix + * @return corresponding encrypted or decrypted text + */ + static const std::valarray mat_mul( + const std::valarray &vector, const matrix &key) { + std::valarray out(vector); // make a copy + + size_t L = std::strlen(STRKEY); + + for (size_t i = 0; i < key.size(); i++) { + int tmp = 0; + for (size_t j = 0; j < vector.size(); j++) { + tmp += key[i][j] * vector[j]; + } + out[i] = static_cast(tmp % L); + } + + return out; + } + + /** + * @brief Get the character at a given index in the ::STRKEY + * + * @param idx index value + * @return character at the index + */ + static inline char get_idx_char(const uint8_t idx) { return STRKEY[idx]; } + + /** + * @brief Get the index of a character in the ::STRKEY + * + * @param ch character to search + * @return index of character + */ + static inline uint8_t get_char_idx(const char ch) { + size_t L = std::strlen(STRKEY); + + for (size_t idx = 0; idx <= L; idx++) + if (STRKEY[idx] == ch) + return idx; + + std::cerr << __func__ << ":" << __LINE__ << ": (" << ch + << ") Should not reach here!\n"; + return 0; + } + + /** + * @brief Convenience function to perform block cipher operations. The + * operations are identical for both encryption and decryption. + * + * @param text input text to encrypt or decrypt + * @param key key for encryption or decryption + * @return encrypted/decrypted output + */ + static const std::string codec(const std::string &text, + const matrix &key) { + size_t text_len = text.length(); + size_t key_len = key.size(); + + // length of output string must be a multiple of key_len + // create output string and initialize with '\0' character + size_t L2 = text_len % key_len == 0 + ? text_len + : text_len + key_len - (text_len % key_len); + std::string coded_text(L2, '\0'); + + // temporary array for batch processing + int i; +#ifdef _OPENMP +#pragma parallel omp for private(i) +#endif + for (i = 0; i < L2 - key_len + 1; i += key_len) { + std::valarray batch_int(key_len); + for (size_t j = 0; j < key_len; j++) { + batch_int[j] = get_char_idx(text[i + j]); + } + + batch_int = mat_mul(batch_int, key); + + for (size_t j = 0; j < key_len; j++) { + coded_text[i + j] = + STRKEY[batch_int[j]]; // get character at key + } + } + + return coded_text; + } + + /** + * Get matrix inverse using Row-transformations. Given matrix must + * be a square and non-singular. + * \returns inverse matrix + **/ + template + static matrix get_inverse(matrix const &A) { + // Assuming A is square matrix + size_t N = A.size(); + + matrix inverse(N, std::valarray(N)); + for (size_t row = 0; row < N; row++) { + for (size_t col = 0; col < N; col++) { + // create identity matrix + inverse[row][col] = (row == col) ? 1.f : 0.f; + } + } + + if (A.size() != A[0].size()) { + std::cerr << "A must be a square matrix!" << std::endl; + return inverse; + } + + // preallocate a temporary matrix identical to A + matrix temp(N, std::valarray(N)); + for (size_t row = 0; row < N; row++) { + for (size_t col = 0; col < N; col++) + temp[row][col] = static_cast(A[row][col]); + } + + // start transformations + for (size_t row = 0; row < N; row++) { + for (size_t row2 = row; row2 < N && temp[row][row] == 0; row2++) { + // this to ensure diagonal elements are not 0 + temp[row] = temp[row] + temp[row2]; + inverse[row] = inverse[row] + inverse[row2]; + } + + for (size_t col2 = row; col2 < N && temp[row][row] == 0; col2++) { + // this to further ensure diagonal elements are not 0 + for (size_t row2 = 0; row2 < N; row2++) { + temp[row2][row] = temp[row2][row] + temp[row2][col2]; + inverse[row2][row] = + inverse[row2][row] + inverse[row2][col2]; + } + } + + if (temp[row][row] == 0) { + // Probably a low-rank matrix and hence singular + std::cerr << "Low-rank matrix, no inverse!" << std::endl; + return inverse; + } + + // set diagonal to 1 + double divisor = temp[row][row]; + temp[row] = temp[row] / divisor; + inverse[row] = inverse[row] / divisor; + // Row transformations + for (size_t row2 = 0; row2 < N; row2++) { + if (row2 == row) + continue; + double factor = temp[row2][row]; + temp[row2] = temp[row2] - factor * temp[row]; + inverse[row2] = inverse[row2] - factor * inverse[row]; + } + } + + return inverse; + } + + static int modulo(int a, int b) { + int ret = a % b; + if (ret < 0) + ret += b; + return ret; + } + + public: + /** + * @brief Generate encryption matrix of a given size. Larger size matrices + * are difficult to generate but provide more security. Important conditions + * are: + * 1. matrix should be invertible + * 2. determinant must not have any common factors with the length of + * character key + * There is no head-fast way to generate hte matrix under the given + * numerical restrictions of the machine but the conditions added achieve + * the goals. Bigger the matrix, greater is the probability of the matrix + * being ill-defined. + * + * @param size size of matrix (typically \f$\text{size}\le10\f$) + * @param limit1 lower limit of range of random elements (default=0) + * @param limit2 upper limit of range of random elements (default=10) + * @return Encryption martix + */ + static matrix generate_encryption_key(size_t size, int limit1 = 0, + int limit2 = 10) { + matrix encrypt_key(size, std::valarray(size)); + matrix min_mat = encrypt_key; + int mat_determinant = -1; // because matrix has only ints, the + // determinant will also be an int + int L = std::strlen(STRKEY); + + double dd; + do { + // keeping the random number range smaller generates better + // defined matrices with more ease of cracking + dd = rand_range(&encrypt_key, limit1, limit2); + mat_determinant = static_cast(dd); + + if (mat_determinant < 0) + mat_determinant = (mat_determinant % L); + } while (std::abs(dd) > 1e3 || // while ill-defined + dd < 0.1 || // while singular or negative determinant + !std::isfinite(dd) || // while determinant is not finite + gcd(mat_determinant, L) != 1); // while no common factors + // std::cout << + + return encrypt_key; + } + + /** + * @brief Generate decryption matrix from an encryption matrix key. + * + * @param encrypt_key encryption key for which to create a decrypt key + * @return Decryption martix + */ + static matrix generate_decryption_key(matrix const &encrypt_key) { + size_t size = encrypt_key.size(); + int L = std::strlen(STRKEY); + + matrix decrypt_key(size, std::valarray(size)); + int det_encrypt = static_cast(determinant_lu(encrypt_key)); + + int mat_determinant = det_encrypt < 0 ? det_encrypt % L : det_encrypt; + + matrix tmp_inverse = get_inverse(encrypt_key); + double d2 = determinant_lu(decrypt_key); + + // find co-prime factor for inversion + int det_inv = -1; + for (int i = 0; i < L; i++) { + if (modulo(mat_determinant * i, L) == 1) { + det_inv = i; + break; + } + } + + if (det_inv == -1) { + std::cerr << "Could not find a co-prime for inversion\n"; + std::exit(EXIT_FAILURE); + } + + mat_determinant = det_inv * det_encrypt; + + // perform modular inverse of encryption matrix + int i; +#ifdef _OPENMP +#pragma parallel omp for private(i) +#endif + for (i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + int temp = std::round(tmp_inverse[i][j] * mat_determinant); + decrypt_key[i][j] = modulo(temp, L); + } + } + return decrypt_key; + } + + /** + * @brief Generate encryption and decryption key pair + * + * @param size size of matrix key (typically \f$\text{size}\le10\f$) + * @param limit1 lower limit of range of random elements (default=0) + * @param limit2 upper limit of range of random elements (default=10) + * @return std::pair, matrix> encryption and decryption + * keys as a pair + * + * @see ::generate_encryption_key + */ + static std::pair, matrix> generate_keys(size_t size, + int limit1 = 0, + int limit2 = 10) { + matrix encrypt_key = generate_encryption_key(size); + matrix decrypt_key = generate_decryption_key(encrypt_key); + double det2 = determinant_lu(decrypt_key); + while (std::abs(det2) < 0.1 || std::abs(det2) > 1e3) { + encrypt_key = generate_encryption_key(size, limit1, limit2); + decrypt_key = generate_decryption_key(encrypt_key); + det2 = determinant_lu(decrypt_key); + } + return std::make_pair(encrypt_key, decrypt_key); + } + + /** + * @brief Encrypt a given text using a given key + * + * @param text string to encrypt + * @param encrypt_key key for encryption + * @return encrypted text + */ + static const std::string encrypt_text(const std::string &text, + const matrix &encrypt_key) { + return codec(text, encrypt_key); + } + + /** + * @brief Decrypt a given text using a given key + * + * @param text string to decrypt + * @param decrypt_key key for decryption + * @return decrypted text + */ + static const std::string decrypt_text(const std::string &text, + const matrix &decrypt_key) { + return codec(text, decrypt_key); + } +}; + +} // namespace ciphers + +/** + * @brief Self test 1 - using 3x3 randomly generated key + * + * @param text string to encrypt and decrypt + */ +void test1(const std::string &text) { + // std::string text = "Hello world!"; + std::cout << "======Test 1 (3x3 key) ======\nOriginal text:\n\t" << text + << std::endl; + + std::pair, matrix> p = + ciphers::HillCipher::generate_keys(3, 0, 100); + matrix ekey = p.first; + matrix dkey = p.second; + + // matrix ekey = {{22, 28, 25}, {5, 26, 15}, {14, 18, 9}}; + // std::cout << "Encryption key: \n" << ekey; + std::string gibberish = ciphers::HillCipher::encrypt_text(text, ekey); + std::cout << "Encrypted text:\n\t" << gibberish << std::endl; + + // matrix dkey = ciphers::HillCipher::generate_decryption_key(ekey); + // std::cout << "Decryption key: \n" << dkey; + std::string txt_back = ciphers::HillCipher::decrypt_text(gibberish, dkey); + std::cout << "Reconstruct text:\n\t" << txt_back << std::endl; + + std::ofstream out_file("hill_cipher_test1.txt"); + out_file << "Block size: " << ekey.size() << "\n"; + out_file << "Encryption Key:\n" << ekey; + out_file << "\nDecryption Key:\n" << dkey; + out_file.close(); + + assert(txt_back == text); + std::cout << "Passed :)\n"; +} + +/** + * @brief Self test 2 - using 8x8 randomly generated key + * + * @param text string to encrypt and decrypt + */ +void test2(const std::string &text) { + // std::string text = "Hello world!"; + std::cout << "======Test 2 (8x8 key) ======\nOriginal text:\n\t" << text + << std::endl; + + std::pair, matrix> p = + ciphers::HillCipher::generate_keys(8, 0, 3); + matrix ekey = p.first; + matrix dkey = p.second; + + std::string gibberish = ciphers::HillCipher::encrypt_text(text, ekey); + std::cout << "Encrypted text:\n\t" << gibberish << std::endl; + + std::string txt_back = ciphers::HillCipher::decrypt_text(gibberish, dkey); + std::cout << "Reconstruct text:\n\t" << txt_back << std::endl; + + std::ofstream out_file("hill_cipher_test2.txt"); + out_file << "Block size: " << ekey.size() << "\n"; + out_file << "Encryption Key:\n" << ekey; + out_file << "\nDecryption Key:\n" << dkey; + out_file.close(); + + assert(txt_back.compare(0, text.size(), text) == 0); + std::cout << "Passed :)\n"; +} + +/** Main function */ +int main() { + std::srand(std::time(nullptr)); + std::cout << "Key dictionary: (" << std::strlen(ciphers::STRKEY) << ")\n\t" + << ciphers::STRKEY << "\n"; + + std::string text = "This is a simple text with numb3r5 and exclamat!0n."; + + test1(text); + test2(text); + + return 0; +}