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