diff options
author | Sadie Powell <sadie@witchery.services> | 2024-03-10 14:50:56 +0000 |
---|---|---|
committer | Sadie Powell <sadie@witchery.services> | 2024-03-10 15:09:38 +0000 |
commit | 9a8cac060d60aee638e368e96afb46967d6d8190 (patch) | |
tree | 32365ea7ebb3e255907e352fcf867877ab9cd6a9 | |
parent | 0353338436141cd27cebd776cbc74e568e801ffe (diff) |
Add support for encrypting passwords with the Argon2 algorithm.
Closes #369.
-rw-r--r-- | .github/workflows/ci-alpine.yml | 3 | ||||
-rw-r--r-- | .github/workflows/ci-linux.yml | 1 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | data/anope.example.conf | 36 | ||||
-rw-r--r-- | include/modules/encryption.h | 6 | ||||
-rw-r--r-- | modules/CMakeLists.txt | 1 | ||||
-rw-r--r-- | modules/database/db_atheme.cpp | 11 | ||||
-rw-r--r-- | modules/extra/enc_argon2.cpp | 193 | ||||
-rw-r--r-- | src/win32/conanfile.txt | 2 |
9 files changed, 252 insertions, 2 deletions
diff --git a/.github/workflows/ci-alpine.yml b/.github/workflows/ci-alpine.yml index a910e45ec..6a0466e44 100644 --- a/.github/workflows/ci-alpine.yml +++ b/.github/workflows/ci-alpine.yml @@ -20,6 +20,7 @@ jobs: echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories apk update apk add \ + argon2-dev \ clang \ cmake \ g++ \ @@ -36,7 +37,7 @@ jobs: - name: Enable extras run: | - for MODULE in ldap mysql regex_pcre2 regex_posix regex_tre sqlite ssl_gnutls ssl_openssl + for MODULE in enc_argon2 ldap mysql regex_pcre2 regex_posix regex_tre sqlite ssl_gnutls ssl_openssl do ln -s $PWD/modules/extra/$MODULE.cpp $PWD/modules done diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 3782e921c..d102be965 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -20,6 +20,7 @@ jobs: g++ \ gettext \ git \ + libargon2-dev \ libgnutls28-dev \ libldap2-dev \ libmysqlclient-dev \ diff --git a/.gitignore b/.gitignore index a2f8a7bbb..cf096b4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build/ config.cache include/sysconf.h +modules/enc_argon2.cpp modules/ldap.cpp modules/mysql.cpp modules/regex_pcre2.cpp diff --git a/data/anope.example.conf b/data/anope.example.conf index 3bc249d24..45ebef058 100644 --- a/data/anope.example.conf +++ b/data/anope.example.conf @@ -1255,6 +1255,42 @@ module } /* + * [EXTRA] enc_argon2 + * + * Provides support for encrypting passwords using the Argon2 algorithm. See + * https://en.wikipedia.org/wiki/Argon2 for more information. + */ +#module +{ + name = "enc_argon2" + + /** The sub-algorithm to use. Can be set to argon2d for Argon2d, argon2i for + * Argon2i, or argon2id for Argon2id. Defaults to argon2id. + */ + #algorithm = "argon2id" + + /** The memory hardness in kibibytes of the Argon2 algorithm. Defaults to + * 128 mebibytes. + */ + #memory_cost = 121072 + + /** The time hardness (iterations) of the Argon2 algorithm. Defaults to 3. + */ + #time_cost = 3 + + /** The amount of parallel threads to use when encrypting passwords. + * Defaults to 1. + */ + #parallelism = 1 + + /** The length in bytes of an Argon2 hash. Defaults to 32. */ + #hash_length = 32 + + /** The length in bytes of an Argon2 salt. Defaults to 32. */ + #salt_length = 32 +} + +/* * enc_bcrypt * * Provides support for encrypting passwords using the Bcrypt algorithm. See diff --git a/include/modules/encryption.h b/include/modules/encryption.h index 0eec23ec3..71d35044a 100644 --- a/include/modules/encryption.h +++ b/include/modules/encryption.h @@ -63,6 +63,12 @@ namespace Encryption virtual ~Provider() = default; + /** Checks whether a plain text value matches a hash created by this provider. */ + virtual bool Compare(const Anope::string &hash, const Anope::string &plain) + { + return hash.equals_cs(plain); + } + /** Creates a new encryption context. */ virtual std::unique_ptr<Context> CreateContext() = 0; diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 8485886e2..c6a98df7f 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -19,6 +19,7 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../conanbuildinfo.cmake") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/extra/${NAME}.cpp" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}") endfunction() + enable_extra("enc_argon2" "ARGON2") enable_extra("mysql" "LIBMYSQLCLIENT") enable_extra("regex_pcre2" "PCRE2") enable_extra("sqlite" "SQLITE3") diff --git a/modules/database/db_atheme.cpp b/modules/database/db_atheme.cpp index d3877b625..5871605a8 100644 --- a/modules/database/db_atheme.cpp +++ b/modules/database/db_atheme.cpp @@ -416,7 +416,7 @@ private: // some of them currently. // // anope-enc-sha256 Converted to enc_sha256 - // argon2 NO + // argon2 Converted to enc_argon2 // base64 Converted to the first encryption algorithm // bcrypt Converted to enc_bcrypt // crypt3-des NO @@ -440,6 +440,15 @@ private: nc->pass = "sha256:" + Anope::Hex(pass) + ":" + Anope::Hex(iv); } + else if (pass.compare(0, 9, "$argon2d$", 9) == 0) + nc->pass = "argon2d:" + pass; + + else if (pass.compare(0, 9, "$argon2i$", 9) == 0) + nc->pass = "argon2i:" + pass; + + else if (pass.compare(0, 10, "$argon2id$", 10) == 0) + nc->pass = "argon2id:" + pass; + else if (pass.compare(0, 8, "$base64$", 8) == 0) { Anope::string rawpass; diff --git a/modules/extra/enc_argon2.cpp b/modules/extra/enc_argon2.cpp new file mode 100644 index 000000000..6454411eb --- /dev/null +++ b/modules/extra/enc_argon2.cpp @@ -0,0 +1,193 @@ +/* Module for providing Argon2 hashing + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * This program is free but copyrighted software; see the file COPYING for + * details. + * + */ + +/* RequiredLibraries: argon2 */ +/* RequiredWindowsLibraries: argon2 */ + +#include <climits> +#include <random> + +#include <argon2.h> + +#include "module.h" +#include "modules/encryption.h" + +class Argon2Context final + : public Encryption::Context +{ +private: + Anope::string buffer; + argon2_type type; + + Anope::string GenerateSalt() + { + static std::random_device device; + static std::mt19937 engine(device()); + static std::uniform_int_distribution<int> dist(CHAR_MIN, CHAR_MAX); + Anope::string saltbuf(this->salt_length, ' '); + for (size_t i = 0; i < this->salt_length; ++i) + saltbuf[i] = static_cast<char>(dist(engine)); + return saltbuf; + } + +public: + static uint32_t memory_cost; + static uint32_t time_cost; + static uint32_t parallelism; + static uint32_t hash_length; + static uint32_t salt_length; + + Argon2Context(argon2_type at) + : type(at) + { + } + + void Update(const unsigned char *data, size_t len) override + { + buffer.append(reinterpret_cast<const char *>(data), len); + } + + Anope::string Finalize() override + { + auto salt = GenerateSalt(); + + // Calculate the size of and allocate the output buffer. + auto length = argon2_encodedlen(this->time_cost, this->memory_cost, this->parallelism, + this->salt_length, this->hash_length, this->type); + + std::vector<char> digest(length); + auto result = argon2_hash(this->time_cost, this->memory_cost, this->parallelism, + buffer.c_str(), buffer.length(), salt.c_str(), salt.length(), nullptr, + this->hash_length, digest.data(), digest.size(), this->type, + ARGON2_VERSION_NUMBER); + + if (result == ARGON2_OK) + return Anope::string(digest.data(), digest.size()); + + Log(LOG_DEBUG_2) << "Argon2 error: " << argon2_error_message(result); + return {}; + } +}; + +uint32_t Argon2Context::memory_cost; +uint32_t Argon2Context::time_cost; +uint32_t Argon2Context::parallelism; +uint32_t Argon2Context::hash_length; +uint32_t Argon2Context::salt_length; + +class Argon2Provider final + : public Encryption::Provider +{ +private: + argon2_type type; + +public: + Argon2Provider(Module *creator, argon2_type at) + : Encryption::Provider(creator, argon2_type2string(at, 0), 0, 0) + , type(at) + { + } + + bool Compare(const Anope::string &hash, const Anope::string &plain) override + { + return argon2_verify(hash.c_str(), plain.c_str(), plain.length(), this->type) == ARGON2_OK; + } + + std::unique_ptr<Encryption::Context> CreateContext() override + { + return std::make_unique<Argon2Context>(this->type); + } +}; + + +class EArgon2 final + : public Module +{ +private: + Encryption::Provider *defaultprovider = nullptr; + Argon2Provider argon2dprovider; + Argon2Provider argon2iprovider; + Argon2Provider argon2idprovider; + + Encryption::Provider *GetAlgorithm(const Anope::string &algorithm) + { + if (algorithm == "argon2d") + return &argon2dprovider; + if (algorithm == "argon2i") + return &argon2iprovider; + if (algorithm == "argon2id") + return &argon2idprovider; + return nullptr; + } + +public: + EArgon2(const Anope::string &modname, const Anope::string &creator) + : Module(modname, creator, ENCRYPTION | VENDOR) + , argon2dprovider(this, Argon2_d) + , argon2iprovider(this, Argon2_i) + , argon2idprovider(this, Argon2_id) + + { + } + + void OnReload(Configuration::Conf *conf) override + { + const auto *block = Config->GetModule(this); + this->defaultprovider = GetAlgorithm(block->Get<const Anope::string>("algorithm", "argon2id")); + Argon2Context::memory_cost = block->Get<uint32_t>("memory_cost", "131072"); + Argon2Context::time_cost = block->Get<uint32_t>("time_cost", "3"); + Argon2Context::parallelism = block->Get<uint32_t>("parallelism", "1"); + Argon2Context::hash_length = block->Get<uint32_t>("hash_length", "32"); + Argon2Context::salt_length = block->Get<uint32_t>("salt_length", "32"); + } + + EventReturn OnEncrypt(const Anope::string &src, Anope::string &dest) override + { + if (!defaultprovider) + return EVENT_CONTINUE; + + auto hash = defaultprovider->Encrypt(src); + auto enc = defaultprovider->name + ":" + hash; + Log(LOG_DEBUG_2) << "(enc_argon2) hashed password from [" << src << "] to [" << enc << "]"; + dest = enc; + return EVENT_ALLOW; + + } + + void OnCheckAuthentication(User *, IdentifyRequest *req) override + { + const auto *na = NickAlias::Find(req->GetAccount()); + if (!na) + return; + + NickCore *nc = na->nc; + auto pos = nc->pass.find(':'); + if (pos == Anope::string::npos) + return; + + Anope::string hash_method(nc->pass.begin(), nc->pass.begin() + pos); + auto provider = GetAlgorithm(hash_method); + if (!provider) + return; // Not a hash for this module. + + Anope::string hash_value(nc->pass.begin() + pos + 1, nc->pass.end()); + if (provider->Compare(hash_value, req->GetPassword())) + { + // If we are NOT the first encryption module or the algorithm is + // different we want to re-encrypt the password with the primary + // encryption method. + if (ModuleManager::FindFirstOf(ENCRYPTION) != this || provider != defaultprovider) + Anope::Encrypt(req->GetPassword(), nc->pass); + req->Success(this); + } + } +}; + +MODULE_INIT(EArgon2) diff --git a/src/win32/conanfile.txt b/src/win32/conanfile.txt index ac72eee2a..d0ef69167 100644 --- a/src/win32/conanfile.txt +++ b/src/win32/conanfile.txt @@ -1,4 +1,5 @@ [requires]
+argon2/20190702
libmysqlclient/8.1.0
openssl/3.2.1
pcre2/10.42
@@ -7,6 +8,7 @@ gettext/0.21 libgettext/0.22
[options]
+argon2/*:shared=True
libmysqlclient/*:shared=True
openssl/*:shared=True
pcre2/*:shared=True
|