From b074b4db01c928a82ee13ec8dc5dc66cb6197ea0 Mon Sep 17 00:00:00 2001 From: Bader Date: Tue, 16 Jul 2024 01:05:33 +0100 Subject: [PATCH] Added Serpent-256-GCM algorithm support --- CHANGELOG.md | 1 + README.md | 1 + src/Algorithm/Algorithm.cpp | 4 + src/Algorithm/Algorithm.hpp | 18 ++- src/Algorithm/Serpent/Serpent.cpp | 244 ++++++++++++++++++++++++++++++ src/Algorithm/Serpent/Serpent.hpp | 33 ++++ 6 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 src/Algorithm/Serpent/Serpent.cpp create mode 100644 src/Algorithm/Serpent/Serpent.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index f27a1f365..3bd583b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 3.2.0 - New Encryption Algorithm `Camelia-256-GCM` +- New Encryption Algorithm `Serpent-256-GCM` - New Hashing Algorithms in `Tools -> Hashing` utility: `SM3`, `RIPEMD128`, `RIPEMD160`, `RIPEMD256`, `RIPEMD256` - Multiple input source `Tools -> Hashing`. Now you can compute the hash of a file alongside text diff --git a/README.md b/README.md index fa4df19c2..55963b057 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ All [Authenticated Encryption](https://www.cryptopp.com/wiki/Authenticated_Encry - :white_check_mark: AES-GCM - :white_check_mark: ChaCha20Poly1305 - :white_check_mark: Camellia-GCM +- :white_check_mark: Serpent-GCM - :white_check_mark: Twofish-GCM - :white_check_mark: IDEA-EAX - :white_check_mark: Blowfish-EAX diff --git a/src/Algorithm/Algorithm.cpp b/src/Algorithm/Algorithm.cpp index 12b545a53..4988e4d6d 100644 --- a/src/Algorithm/Algorithm.cpp +++ b/src/Algorithm/Algorithm.cpp @@ -9,6 +9,7 @@ #include "Twofish/Twofish.hpp" #include "RSA/RSA.hpp" #include "Camellia/Camellia.hpp" +#include "Serpent/Serpent.hpp" #include #include #include @@ -49,6 +50,8 @@ std::unique_ptr Algorithm::CreateFromName(const std::string& algorith return std::make_unique(intent); else if (AlgoIn({"camellia", "camellia-256", "camellia-gcm", "cameliagcm"})) return std::make_unique(intent); + else if (AlgoIn({"serpent", "serpent-256", "serpent-gcm", "serpentgcm"})) + return std::make_unique(intent); else throw std::runtime_error("Unsupported algorithm: " + algorithm_name); } @@ -77,6 +80,7 @@ std::string Algorithm::AlgoTypeEnumToStr(const Algorithm::Type e) noexcept { CASE_RET(ChaCha20Poly1305); CASE_RET(RSA); CASE_RET(Camellia); + CASE_RET(Serpent); default: return ""; } diff --git a/src/Algorithm/Algorithm.hpp b/src/Algorithm/Algorithm.hpp index 5d9d04c5a..8189d1456 100644 --- a/src/Algorithm/Algorithm.hpp +++ b/src/Algorithm/Algorithm.hpp @@ -25,6 +25,7 @@ #include // RSA #include // Twofish #include // Camellia +#include // Serpent #pragma warning(pop) static_assert(sizeof(Enigma::byte) == sizeof(CryptoPP::byte), "Enigma byte size must be the same size with Crypto++'s byte"); @@ -42,6 +43,7 @@ class IDEA; class ChaCha20Poly1305; class RSA; class Camellia; +class Serpent; /** * @brief Algorithm abstract class @@ -76,12 +78,24 @@ class Algorithm { Blowfish, // Blowfish-EAX IDEA, // IDEA-EAX ChaCha20Poly1305, // ChaCha20Poly1305 - RSA, // RSA-OAEP-SHA256 - Camellia, // Camellia-GCM + RSA, // RSA-OAEP-SHA256 + Camellia, // Camellia-GCM + Serpent, // Serpent-GCM ENIGMA_ENUM_DECLARE_BEGIN_END(AES) }; + inline static const std::unordered_map ALGORITHM_DESCRIPTIONS = { + {Type::AES, "Symmetric, extremely high security, efficient, widely supported"}, + {Type::Twofish, "Symmetric, high security, flexible and efficient"}, + {Type::TripleDES, "Symmetric, moderate security, suitable for legacy systems"}, + {Type::Blowfish, "Symmetric, moderate security, fast and simple"}, + {Type::IDEA, "Symmetric, moderate to high security, simple and efficient"}, + {Type::ChaCha20Poly1305, "Symmetric, very high security, high performance in software"}, + {Type::RSA, "Asymmetric, very high security, excellent for key exchange"}, + {Type::Camellia, "Symmetric, very high security, comparable to AES"}, + {Type::Serpent, "Symmetric, high security, conservative design"}, + }; public: explicit Algorithm(const Type type, const Intent intent = Intent::Encrypt | Intent::Decrypt) noexcept; diff --git a/src/Algorithm/Serpent/Serpent.cpp b/src/Algorithm/Serpent/Serpent.cpp new file mode 100644 index 000000000..e9f3b36ef --- /dev/null +++ b/src/Algorithm/Serpent/Serpent.cpp @@ -0,0 +1,244 @@ +#include +#include "Serpent.hpp" + +NS_ENIGMA_BEGIN + +Serpent::Serpent(const Algorithm::Intent intent) noexcept + : Algorithm(Algorithm::Type::Serpent, intent) { + if (static_cast(intent & Intent::Encrypt)) + m_serpent_encryptor = std::make_unique::Encryption>(); + if (static_cast(intent & Intent::Decrypt)) + m_serpent_decryptor = std::make_unique::Decryption>(); +} + +Serpent::~Serpent() noexcept { +} + +std::vector Serpent::Encrypt(const std::string& password, const byte *buffer, const std::size_t buffSize) { + ENIGMA_ASSERT_OR_THROW(m_serpent_encryptor, "Serpent Encryptor is not initialized properly"); + ENIGMA_ASSERT_OR_THROW(password.size() >= Constants::Algorithm::MINIMUM_PASSWORD_LENGTH, "For security reasons, Serpent minimum password length is " + std::to_string(Constants::Algorithm::MINIMUM_PASSWORD_LENGTH)); + + Meta::EnigmaFooter footer{}; + footer.magic = Meta::ENIGMA_MAGIC; + footer.version = ENIGMA_VERSION_MAJOR * 100'000 + ENIGMA_VERSION_MINOR * 1000 + ENIGMA_VERSION_PATCH; + footer.algo = this->GetType(); + footer.iv = Algorithm::GenerateRandomIV(CryptoPP::Serpent::BLOCKSIZE); + footer.hash = HashUtils::bytes(buffer, buffSize); + footer.extra = {}; + + // Encryption key to be generated from user password + IV + CryptoPP::SecByteBlock key(static_cast(CryptoPP::Serpent::MAX_KEYLENGTH) + static_cast(CryptoPP::Serpent::BLOCKSIZE)); + // Convert key to KDF SHA-256, which allows you to use a password smaller or larger than the cipher's key size + CryptoPP::HKDF hkdf{}; + hkdf.DeriveKey( + key, key.size(), + reinterpret_cast(password.data()), password.size(), + reinterpret_cast(footer.iv.data()), footer.iv.size(), + nullptr, 0); + // Set Key and IV to the encryptor + m_serpent_encryptor->SetKeyWithIV(key, static_cast(CryptoPP::Serpent::MAX_KEYLENGTH), key + static_cast(CryptoPP::Serpent::MAX_KEYLENGTH)); // key, kl, iv, ivl + + // Encrypt + std::vector cipher; + const CryptoPP::ArraySource vs( + buffer, + buffSize, + true, + new CryptoPP::AuthenticatedEncryptionFilter( // note: for GCM mode, use AuthenticatedEncryptionFilter instead of StreamTransformationFilter + *m_serpent_encryptor, + new CryptoPP::VectorSink(cipher))); //NOTE: VectorSource will auto clean the allocated memory + + std::vector out; + // Append Cipher + out.insert(out.end(), cipher.begin(), cipher.end()); + // Append EnigmaFooter + std::vector footerBytes = footer.toBytes(); + out.insert(out.end(), footerBytes.begin(), footerBytes.end()); + return out; +} + +std::vector Serpent::Encrypt(const std::string& password, const std::vector& buffer) { + return Encrypt(password, buffer.data(), buffer.size()); +} + +std::vector Serpent::Decrypt(const std::string& password, const byte *cipher, const std::size_t cipherSize) { + ENIGMA_ASSERT_OR_THROW(Meta::isEnigmaCipher(cipher, cipherSize), "Given cipher is malformed or was not encrypted with Enigma"); + ENIGMA_ASSERT_OR_THROW(m_serpent_decryptor, "Serpent Decryptor is not initialized properly"); + + // Extract footer + Meta::EnigmaFooter footer = Meta::EnigmaFooter::fromBytes(cipher, cipherSize); + // Prepare Key + CryptoPP::SecByteBlock key(static_cast(CryptoPP::Serpent::MAX_KEYLENGTH) + static_cast(CryptoPP::Serpent::BLOCKSIZE)); + // Convert key to KDF SHA-256, which allows you to use a password smaller or larger than the cipher's key size + CryptoPP::HKDF hkdf; + hkdf.DeriveKey( + key, key.size(), + reinterpret_cast(password.data()), password.size(), + reinterpret_cast(footer.iv.data()), footer.iv.size(), + nullptr, 0); + + // Set Key and IV to the decrypter + m_serpent_decryptor->SetKeyWithIV(key, static_cast(CryptoPP::Serpent::MAX_KEYLENGTH), key + static_cast(CryptoPP::Serpent::MAX_KEYLENGTH)); // key, kl, iv, ivl + + // Decrypt + std::vector decrypted; + [[maybe_unused]] const auto ss = CryptoPP::ArraySource( + cipher, + cipherSize - footer.sizeInBytes(), + true, + new CryptoPP::AuthenticatedDecryptionFilter( + *m_serpent_decryptor, + new CryptoPP::VectorSink(decrypted))); + + // Ensure decryption is successful + ENIGMA_ASSERT_OR_THROW(HashUtils::bytes(decrypted) == footer.hash, "Decryption failure. Original SHA256 hash of buffer does not match decrypted hash"); + return decrypted; +} + +std::vector Serpent::Decrypt(const std::string& password, const std::vector& cipher) { + return Decrypt(password, cipher.data(), cipher.size()); +} + + +void Serpent::Encrypt(const std::string& password, const fs::path& in_filename, const fs::path& out_filename) { + ENIGMA_ASSERT_OR_THROW(!Meta::isEnigmaFile(in_filename), in_filename.filename().string() + " is already encrypted with Enigma"); + ENIGMA_ASSERT_OR_THROW(password.size() >= Constants::Algorithm::MINIMUM_PASSWORD_LENGTH, "Serpent Minimum Password Length is " + std::to_string(Constants::Algorithm::MINIMUM_PASSWORD_LENGTH)); + ENIGMA_ASSERT_OR_THROW(m_serpent_encryptor, "Serpent Encryptor is not initialized properly"); + + Meta::EnigmaFooter footer{}; + footer.magic = Meta::ENIGMA_MAGIC; + footer.version = ENIGMA_VERSION_MAJOR * 100'000 + ENIGMA_VERSION_MINOR * 1000 + ENIGMA_VERSION_PATCH; + footer.algo = this->GetType(); + footer.iv = Algorithm::GenerateRandomIV(CryptoPP::Serpent::BLOCKSIZE); + footer.extra = {}; + + // Encryption key to be generated from user password + IV + CryptoPP::SecByteBlock key(static_cast(CryptoPP::Serpent::MAX_KEYLENGTH) + static_cast(CryptoPP::Serpent::BLOCKSIZE)); + // Convert key to KDF SHA-256, which allows you to use a password smaller or larger than the cipher's key size + CryptoPP::HKDF hkdf{}; + hkdf.DeriveKey( + key, key.size(), + reinterpret_cast(password.data()), password.size(), + reinterpret_cast(footer.iv.data()), footer.iv.size(), + nullptr, 0); + + // Now encrypt file chunk by chunk + std::ofstream ofs{out_filename, std::ios::binary}; + CryptoPP::SHA256 sha256{}; + bool ok = ofs.good(); + FileUtils::ReadChunks(in_filename, Meta::ENIGMA_BUFFER_DEFAULT_SIZE, [this, &ok, &ofs, &key, &sha256](std::vector&& chunk) -> bool { + try { + // Encrypt chunk + // Set Key and IV to the encryptor + m_serpent_encryptor->SetKeyWithIV(key, static_cast(CryptoPP::Serpent::MAX_KEYLENGTH), key + static_cast(CryptoPP::Serpent::MAX_KEYLENGTH)); // key, kl, iv, ivl + + Meta::EnigmaCipherChunk cipherChunk{}; + cipherChunk.magic = Meta::ENIGMA_CIPHER_CHUNK_MAGIC; + const CryptoPP::VectorSource vs( + chunk, + true, + new CryptoPP::AuthenticatedEncryptionFilter( // note: for GCM mode, use AuthenticatedEncryptionFilter instead of StreamTransformationFilter + *m_serpent_encryptor, + new CryptoPP::VectorSink(cipherChunk.cipher))); //NOTE: VectorSource will auto clean the allocated memory + // Save chunk bytes to ofs + const std::vector cipherChunkBytes = cipherChunk.toBytes(); + ok &= ofs.write(reinterpret_cast(cipherChunkBytes.data()), cipherChunkBytes.size()).good(); + // Also profit from the occasion to calculate sha256 hash + sha256.Update(chunk.data(), chunk.size()); + return ok; + } catch (const CryptoPP::Exception& e) { + ok &= false; + ENIGMA_ERROR("Could not encrypt chunk: {}", e.what()); + } catch (const std::exception& e) { + ok &= false; + ENIGMA_ERROR("Could not encrypt chunk: {}", e.what()); + } catch (...) { + ok &= false; + ENIGMA_ERROR("Could not encrypt chunk: UNKNOWN ERROR"); + } + return ok; + }); + + // No need to keep out_filename on failed encryption + if (!ok) { + ofs.close(); + fs::remove(out_filename); + throw std::runtime_error("Could not encrypt file " + in_filename.string()); + } + + // Alles gut! + // Append enigma footer info + ofs.seekp(0, std::ios::end); + sha256.Final(footer.hash.data()); + std::vector footerBytes = footer.toBytes(); + ofs.write(reinterpret_cast(footerBytes.data()), footerBytes.size()); + ENIGMA_ASSERT_OR_THROW(ofs.good(), "Failed to write footer bytes"); + ofs.close(); +} + +void Serpent::Decrypt(const std::string& password, const fs::path& in_filename, const fs::path& out_filename) { + ENIGMA_ASSERT_OR_THROW(Meta::isEnigmaFile(in_filename), "Given file " + in_filename.string() + " is malformed or was not encrypted with Enigma"); + ENIGMA_ASSERT_OR_THROW(m_serpent_decryptor, "Serpent Decryptor is not initialized properly"); + + // Extract footer from encrypted file + Meta::EnigmaFooter footer = Meta::EnigmaFooter::fromFile(in_filename); + // Prepare Key + CryptoPP::SecByteBlock key(static_cast(CryptoPP::Serpent::MAX_KEYLENGTH) + static_cast(CryptoPP::Serpent::BLOCKSIZE)); + // Convert key to KDF SHA-256, which allows you to use a password smaller or larger than the cipher's key size + CryptoPP::HKDF hkdf; + hkdf.DeriveKey( + key, key.size(), + reinterpret_cast(password.data()), password.size(), + reinterpret_cast(footer.iv.data()), footer.iv.size(), + nullptr, 0); + + // Decrypt file chunk by chunk + std::ofstream ofs{out_filename, std::ios::binary}; + CryptoPP::SHA256 sha256; + bool ok = ofs.good(); + Meta::readCipherChunks(in_filename, [this, &ok, &ofs, &key, &sha256](Meta::EnigmaCipherChunk&& cipherChunk) -> bool { + try { + // Decrypt chunk + // Set Key and IV to the decrypter + m_serpent_decryptor->SetKeyWithIV(key, static_cast(CryptoPP::Serpent::MAX_KEYLENGTH), key + static_cast(CryptoPP::Serpent::MAX_KEYLENGTH)); // key, kl, iv, ivl + + std::vector decrypted{}; + [[maybe_unused]] const auto ss = CryptoPP::VectorSource( + cipherChunk.cipher, + true, + new CryptoPP::AuthenticatedDecryptionFilter( + *m_serpent_decryptor, + new CryptoPP::VectorSink(decrypted))); + // Write decrypted chunk to ofs + ok &= ofs.write(reinterpret_cast(decrypted.data()), decrypted.size()).good(); + // Also profit from the occasion to calculate sha256 hash + sha256.Update(decrypted.data(), decrypted.size()); + return ok; + } catch (const CryptoPP::Exception& e) { + ok &= false; + ENIGMA_ERROR("Could not decrypt chunk: {}", e.what()); + } catch (const std::exception& e) { + ok &= false; + ENIGMA_ERROR("Could not decrypt chunk: {}", e.what()); + } catch (...) { + ok &= false; + ENIGMA_ERROR("Could not decrypt chunk: UNKNOWN ERROR"); + } + return ok; + }); + // No need to keep out_filename on failed decryption + if (!ok) { + ofs.close(); + fs::remove(out_filename); + throw std::runtime_error("Could not decrypt file " + in_filename.string()); + } + ofs.close(); + + // Ensure decryption is successful + std::array digest{}; + sha256.Final(digest.data()); + ENIGMA_INFO("Verifying SHA256 hash of {} ...", out_filename.filename().string()); + ENIGMA_ASSERT_OR_THROW(digest == footer.hash, "Decryption failure. Original SHA256 hash of file does not match decrypted hash"); +} + +NS_ENIGMA_END diff --git a/src/Algorithm/Serpent/Serpent.hpp b/src/Algorithm/Serpent/Serpent.hpp new file mode 100644 index 000000000..26817f44c --- /dev/null +++ b/src/Algorithm/Serpent/Serpent.hpp @@ -0,0 +1,33 @@ +#pragma once +#include + +NS_ENIGMA_BEGIN +/** +* Serpent-GCM Algorithm Encryptor/Decryptor +* +* Serpent is a block cipher designed by Ross Anderson, Eli Biham, and Lars Knudsen. +* It was ranked 2nd in the Advanced Encryption Standard contest. +* The Serpent homepage is located at http://www.cl.cam.ac.uk/~rja14/serpent.html. +*/ +class Serpent : public Algorithm { + public: + /** + * @param intent: Operation, Encrypt or Decrypt + */ + explicit Serpent(const Algorithm::Intent intent) noexcept; + ~Serpent() noexcept override; + + public: + std::vector Encrypt(const std::string& password, const byte *buffer, const std::size_t buffSize) override; + std::vector Encrypt(const std::string& password, const std::vector& buffer) override; + std::vector Decrypt(const std::string& password, const byte *cipher, const std::size_t cipherSize) override; + std::vector Decrypt(const std::string& password, const std::vector& cipher) override; + void Encrypt(const std::string& password, const fs::path& in_filename, const fs::path& out_filename) override; + void Decrypt(const std::string& password, const fs::path& in_filename, const fs::path& out_filename) override; + + private: + std::unique_ptr::Encryption> m_serpent_encryptor; /**< Serpent-GCM encryptor */ + std::unique_ptr::Decryption> m_serpent_decryptor; /**< Serpent-GCM decryptor */ +}; + +NS_ENIGMA_END