/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2025 Sadie Powell * * This file is part of InspIRCd. InspIRCd is free software: you can * redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 2. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "stringutils.h" #include "utility/string.h" namespace Hash { class Context; class Provider; class ProviderRef; class HMACContext; class HMACProvider; /** Compares a password to a hashed password. * @param password The hashed password. * @param algorithm If non-empty then the algorithm the password is hashed with. * @param value The value to check to see if the password is valid. * @return True if the password is correct, otherwise, false. */ inline bool CheckPassword(const std::string& password, const std::string& algorithm, const std::string& value); /** Generates a hash-based message authentication code. * @param prov The hash algorithm to hash with. * @param key The secret key. * @param data The data to hash. */ inline std::string HMAC(Hash::Provider* prov, const std::string& key, const std::string& data); } /** Base class for hash contexts. */ class Hash::Context { public: virtual ~Context() = default; /** Updates the hash context with the specified data. * @param str The data to update the context with. */ inline void Update(const std::string& str) { Update(reinterpret_cast(str.c_str()), str.length()); } /** Updates the hash context with the specified data. * @param data The data to update the context with. * @param len The length of the data. */ virtual void Update(const unsigned char *data, size_t len) = 0; /** Finalises the hash context and returns the digest. */ virtual std::string Finalize() = 0; }; /** Provider of hash contexts. */ class Hash::Provider : public DataProvider { public: /** The byte size of the block cipher. */ const size_t block_size; /** The byte size of the resulting digest. */ const size_t digest_size; /** Creates a provider of hash contexts. * @param mod The module that created this provider. * @param algorithm The name of the hash algorithm. * @param ds The byte size of the resulting digest or 0 if it is variable length. * @param bs The byte size of the block cipher or 0 if not a block cipher. */ Provider(Module* mod, const std::string& algorithm, size_t ds = 0, size_t bs = 0) : DataProvider(mod, "hash/" + algorithm) , block_size(bs) , digest_size(ds) { } virtual ~Provider() = default; /** Checks whether a plain text value matches a hash created by this provider * @param hash The hashed value to compare against. * @param plain The plain text password to compare. */ virtual bool Compare(const std::string& hash, const std::string& plain) { return !hash.empty() && InspIRCd::TimingSafeCompare(hash, ToPrintable(Hash(plain))); } /** Called on initialising a hash provider to check it works properly. * @param checks A map of known ciphertexts to plaintexts. */ inline void Check(const std::map& checks) { for (const auto& [hash, plain] : checks) { if (!Compare(hash, plain)) throw ModuleException(creator, "BUG: unable to generate {} hashes safely! Please report this!", GetAlgorithm()); } ServerInstance->Logs.Debug("HASH", "The {} hash provider appears to be working correctly.", GetAlgorithm()); } /** Creates a new hash context. */ virtual std::unique_ptr CreateContext() = 0; /** Retrieves the name of the hash algorithm. */ const char* GetAlgorithm() const { return name.c_str() + 5; } /** Quickly hashes the specified values and returns the digest. */ template std::string Hash(Args&& ...args) { auto context = CreateContext(); context->Update(std::forward(args)...); return context->Finalize(); } /** Determines whether this hash algorithm is a key derivation function. */ auto IsKDF() const { return block_size == 0; } /** Determines whether this hash provider is safe for password hashing. */ virtual bool IsPasswordSafe() const { return true; } /** Converts a hash to its printable form. */ virtual std::string ToPrintable(const std::string& hash) { return Hex::Encode(hash); } }; /** Holds a dynamic reference to a hash provider. */ class Hash::ProviderRef final : public dynamic_reference_nocheck { public: /** Holds a dynamic reference to a hash algorithm. * @param mod The module that created this reference. * @param algorithm The name of the hash algorithm. */ ProviderRef(Module* mod, const std::string& algorithm) : dynamic_reference_nocheck(mod, "hash/" + algorithm) { } /** Retrieves the name of the referenced hash algorithm. */ const char* GetAlgorithm() const { if (GetProvider().empty()) return nullptr; return GetProvider().c_str() + 5; } }; /** Provides a hash context for HMAC generation. */ class Hash::HMACContext final : public Hash::Context { private: /** The data which has been written to this context. */ std::string buffer; /** The underlying hash provider. */ Hash::ProviderRef provider; /** Generates a salt for the HMAC. */ std::string GenerateSalt() { if (!provider || provider->IsKDF()) return {}; std::vector salt(provider->digest_size); ServerInstance->GenRandom(salt.data(), salt.size()); return std::string(salt.data(), salt.size()); } public: /** Creates HMAC hash context. * @param prov The underlying hash provider. */ HMACContext(const Hash::ProviderRef& prov) : provider(prov) { } /** @copydoc Hash::HMACContext::Update */ void Update(const unsigned char *data, size_t len) override { buffer.append(reinterpret_cast(data), len); } /** @copydoc Hash::HMACContext::Finalize */ std::string Finalize() override { if (!provider) return {}; // No underlying hash (should never happen). auto salt = this->GenerateSalt(); if (salt.empty()) return {}; auto hash = Hash::HMAC(*provider, salt, buffer); if (hash.empty()) return {}; this->buffer.clear(); return FMT::format("{}${}", Base64::Encode(salt), Base64::Encode(hash)); } }; /** Provides a hash provider for HMAC generation. */ class Hash::HMACProvider final : public Hash::Provider { private: /** The underlying hash provider. */ Hash::ProviderRef provider; public: /** Creates a provider of HMAC hash contexts. * @param mod The module that created this provider. * @param algorithm The name of the hash algorithm. */ HMACProvider(Module* mod, const std::string& algorithm) : Hash::Provider(mod, FMT::format("hmac-{}", algorithm)) , provider(mod, algorithm) { } /** @copydoc Hash::Provider::Compare */ bool Compare(const std::string& hash, const std::string& plain) override { if (!provider) return false; // No underlying hash (should never happen). auto sep = hash.find('$'); if (sep == std::string::npos) return false; // Malformed hash. auto rawkey = Base64::Decode(hash.substr(0, sep)); auto rawhash = Base64::Decode(hash.substr(sep + 1)); auto expected = Hash::HMAC(*provider, rawkey, plain); return !expected.empty() && InspIRCd::TimingSafeCompare(rawhash, expected); } /** @copydoc Hash::Provider::CreateContext */ std::unique_ptr CreateContext() override { return std::make_unique(provider); } /** @copydoc Hash::Provider::ToPrintable */ std::string ToPrintable(const std::string &hash) override { // We have no way to make this printable without the creating context // so we always return the printed form. return hash; } }; inline bool Hash::CheckPassword(const std::string& password, const std::string& algorithm, const std::string& value) { auto* hash = ServerInstance->Modules.FindDataService("hash/" + algorithm); if (hash) return hash->Compare(password, value); // The hash algorithm wasn't provided by any modules. If its plain // text then we can check it internally. if (algorithm.empty() || insp::equalsci(algorithm, "plaintext")) return InspIRCd::TimingSafeCompare(password, value); ServerInstance->Logs.Debug("HASH", "Unable to check password hashed with an unknown algorithm: {}", algorithm); return false; } inline std::string Hash::HMAC(Hash::Provider* prov, const std::string& key, const std::string& data) { if (!prov || prov->IsKDF()) return {}; auto keybuf = key.length() > prov->block_size ? prov->Hash(key) : key; keybuf.resize(prov->block_size); std::string hmac1; std::string hmac2; for (size_t i = 0; i < prov->block_size; ++i) { hmac1.push_back(static_cast(keybuf[i] ^ 0x5C)); hmac2.push_back(static_cast(keybuf[i] ^ 0x36)); } hmac2.append(data); hmac1.append(prov->Hash(hmac2)); return prov->Hash(hmac1); }