1
0
mirror of https://github.com/anope/anope.git synced 2026-06-12 17:04:47 +02:00
Files
2026-01-01 18:07:12 +00:00

194 lines
5.3 KiB
C++

// Anope IRC Services <https://www.anope.org/>
//
// Copyright (C) 2003-2026 Anope Contributors
//
// Anope is free software. You can use, modify, and/or distribute it under the
// terms of version 2 of the GNU General Public License. See docs/LICENSE.txt
// for the complete terms of this license and docs/AUTHORS.txt for a list of
// contributors.
//
// Based on the original code of Epona by Lara
// Based on the original code of Services by Andy Church
//
// SPDX-License-Identifier: GPL-2.0-only
#include <climits>
#include <random>
#include "bcrypt/crypt_blowfish.c"
#include "module.h"
#include "modules/encryption.h"
class BCryptContext final
: public Encryption::Context
{
private:
Anope::string buffer;
Anope::string GenerateSalt()
{
static std::random_device device;
static std::mt19937 engine(device());
static std::uniform_int_distribution<int> dist(CHAR_MIN, CHAR_MAX);
char entropy[16];
for (size_t i = 0; i < sizeof(entropy); ++i)
entropy[i] = static_cast<char>(dist(engine));
char salt[32];
if (!_crypt_gensalt_blowfish_rn("$2a$", rounds, entropy, sizeof(entropy), salt, sizeof(salt)))
{
Log(LOG_DEBUG) << "Unable to generate a salt for Bcrypt: " << strerror(errno);
return {};
}
return salt;
}
public:
static unsigned long rounds;
static Anope::string Hash(const Anope::string &data, const Anope::string &salt)
{
char hash[64];
if (!_crypt_blowfish_rn(data.c_str(), salt.c_str(), hash, sizeof(hash)))
{
Log(LOG_DEBUG) << "Unable to generate a hash for Bcrypt: " << strerror(errno);
return {};
}
return hash;
}
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();
if (salt.empty())
return {};
return Hash(this->buffer, salt);
}
};
unsigned long BCryptContext::rounds = 10;
class BCryptProvider final
: public Encryption::Provider
{
public:
BCryptProvider(Module *creator)
: Encryption::Provider(creator, "bcrypt", 0, 0)
{
}
bool Compare(const Anope::string &hash, const Anope::string &plain) override
{
auto newhash = BCryptContext::Hash(plain, hash);
return !newhash.empty() && hash.equals_cs(newhash);
}
std::unique_ptr<Encryption::Context> CreateContext() override
{
return std::make_unique<BCryptContext>();
}
Anope::string ToPrintable(const Anope::string &hash) override
{
// The crypt_blowfish library does not expose a raw form.
return hash;
}
};
class EBCrypt final
: public Module
{
private:
BCryptProvider bcryptprovider;
static const size_t BCRYPT_MAX_LEN = 72;
public:
EBCrypt(const Anope::string &modname, const Anope::string &creator)
: Module(modname, creator, ENCRYPTION | VENDOR)
, bcryptprovider(this)
{
bcryptprovider.Check({
{ "$2a$10$c9lUAuJmTYXEfNuLOiyIp.lZTMM.Rw5qsSAyZhvGT9EC3JevkUuOu", "" },
{ "$2a$10$YV4jDSGs0ZtQbpL6IHtNO.lt5Q.uzghIohCcnERQVBGyw7QJMfyhe", "The quick brown fox jumps over the lazy dog" },
});
}
EventReturn OnEncrypt(const Anope::string &src, Anope::string &dest) override
{
if (src.length() > BCRYPT_MAX_LEN)
return EVENT_CONTINUE;
dest = "bcrypt:" + bcryptprovider.Encrypt(src);
Log(LOG_DEBUG_2) << "(enc_bcrypt) hashed password from [" << src << "] to [" << dest << "]";
return EVENT_ALLOW;
}
void OnCheckAuthentication(User *, IdentifyRequest *req) override
{
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);
if (!hash_method.equals_cs("bcrypt"))
return;
Anope::string hash_value(nc->pass.begin() + pos + 1, nc->pass.end());
if (bcryptprovider.Compare(hash_value, req->GetPassword()))
{
unsigned long rounds = 0;
// Try to extract the rounds count to check if we need to
// re-encrypt the password.
pos = hash_value.find('$', 4);
if (pos != Anope::string::npos)
rounds = Anope::Convert<unsigned long>(hash_value.substr(4, pos - 4), 0);
if (!rounds)
Log(LOG_DEBUG) << "Unable to determine the rounds of a bcrypt hash: " << hash_value;
// If we are NOT the first encryption module or the Bcrypt rounds
// are different we want to re-encrypt the password with the primary
// encryption method.
if (ModuleManager::FindFirstOf(ENCRYPTION) != this || (rounds && rounds != BCryptContext::rounds))
Anope::Encrypt(req->GetPassword(), nc->pass);
req->Success(this, na);
}
}
void OnReload(Configuration::Conf &conf) override
{
const auto maxpasslen = conf.GetModule("nickserv").Get<unsigned>("maxpasslen", "50");
if (maxpasslen > BCRYPT_MAX_LEN && ModuleManager::FindFirstOf(ENCRYPTION) == this)
Log(this) << "Warning: {nickserv}:maxpasslen is set to " << maxpasslen << " which is longer than the bcrypt maximum length of " << BCRYPT_MAX_LEN;
auto &block = conf.GetModule(this);
auto rounds = block.Get<unsigned long>("rounds", "10");
if (rounds < 10 || rounds > 32)
{
Log(this) << "Bcrypt rounds MUST be between 10 and 32 inclusive; using 10 instead of " << rounds << '.';
BCryptContext::rounds = 10;
return;
}
if (rounds > 14)
Log(this) << "Bcrypt rounds higher than 14 are very CPU intensive; are you sure you want to use " << rounds << '?';
BCryptContext::rounds = rounds;
}
};
MODULE_INIT(EBCrypt)