/*
* InspIRCd -- Internet Relay Chat Daemon
*
* Copyright (C) 2025 Sadie Powell <sadie@witchery.services>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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<const unsigned char *>(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<std::string, std::string>& 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<Context> 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<typename... Args>
std::string Hash(Args&& ...args)
{
auto context = CreateContext();
context->Update(std::forward<Args>(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<Hash::Provider>
{
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<Hash::Provider>(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<char> 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<const char *>(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<Context> CreateContext() override
{
return std::make_unique<HMACContext>(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::Provider>("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<char>(keybuf[i] ^ 0x5C));
hmac2.push_back(static_cast<char>(keybuf[i] ^ 0x36));
}
hmac2.append(data);
hmac1.append(prov->Hash(hmac2));
return prov->Hash(hmac1);
}