aboutsummaryrefslogtreecommitdiff
/*
 * 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/>.
 */


#include "inspircd.h"
#include "modules/hash.h"

namespace
{
	// NOTE: We use an extremely low iteration count in the checks to
	// avoid blocking for a long time when starting up.
	const std::map<std::string, std::map<std::string, std::string>> checks = {
		{ "sha1", {
			{ "100:bsOVI55xIWFZxO7Ox4lb8Q:B9baArs7lJR9kFVLFAbpEA", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha224", {
			{ "100:7mCEabsDkJmv7yphDJ00Zg:ZpVPEq7DshpaCwPpE6WiTA", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha256", {
			{ "100:eV0OGnSmBtvC1Y7pUc5Odg:I4yDKQWppE2rrHJDX01V2g", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha384", {
			{ "100:uj0W7gdL6BlHRst3jJwaFA:8FtaHvqYgZwsSLc2a2Fiig", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha512", {
			{ "100:OOjcsv69e87ItbWU6+TX0w:5VoQbRnulaCeJlbA44UMFw", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha3-224", {
			{ "100:iqGZHw2Yl6uZm4FDpa/VIA:iP7f+O2eQahVbgTSqWzC6A", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha3-256", {
			{ "100:Fd/ckU57sD6Q3PVx0CueWw:U6tmB9q9t55cxNK3I/cHcg", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha3-384", {
			{ "100:93wezDugG6v2bB/4dqkhGw:1DVYoIRZELyC/Ed0FyzfuA", "The quick brown fox jumps over the lazy dog" },
		}},
		{ "sha3-512", {
			{ "100:7jK7DPg5yLqNzCRbqvAxYA:hhPbmAnTi0l7xs1aiu4JAw", "The quick brown fox jumps over the lazy dog" },
		}},
	};

	std::string PBKDF2(Hash::Provider* provider, const std::string& plain, const std::string& salt, uint32_t iterations, size_t length)
	{
		if (!provider || provider->IsKDF())
			return {};

		std::string output;
		auto blocks = static_cast<size_t>(std::ceil(static_cast<double>(length) / provider->digest_size));
		for (size_t block = 1; block <= blocks; ++block)
		{
			auto block_data = salt;
			for (size_t i = 0; i < 4; ++i)
				block_data.push_back(block >> (24 - i * 8) & 0x0F);

			auto block_hash = Hash::HMAC(provider, plain, block_data);
			auto prevhash = block_hash;
			for (size_t iteration = 1; iteration < iterations; ++iteration)
			{
				auto hash = Hash::HMAC(provider, plain, prevhash);
				for (size_t i = 0; i < provider->digest_size; ++i)
					block_hash[i] ^= hash[i];
				prevhash = hash;
			}
			output += block_hash;
		}

		output.erase(length);
		return output;
	}
}

struct PBKDF2Config final
{
	uint32_t iterations = 0;
	size_t key_length = 0;
	size_t salt_length = 0;

	void ReadConfig(const std::shared_ptr<ConfigTag>& tag, PBKDF2Config* def = nullptr)
	{
		iterations = tag->getNum<uint32_t>("iterations", def ? def->iterations : 15'000, 1000);
		key_length = tag->getNum<size_t>("length", def ? def->key_length : 32, 1, 1024);
		salt_length = tag->getNum<size_t>("saltlength", def ? def->salt_length : key_length, 1, 1024);
	}
};

class PBKDF2Context final
	: public Hash::Context
{
private:
	std::string buffer;
	const PBKDF2Config& config;
	Hash::ProviderRef provider;

	std::string GenerateSalt()
	{
		std::vector<char> salt(config.salt_length);
		ServerInstance->GenRandom(salt.data(), salt.size());
		return std::string(salt.data(), salt.size());
	}

public:
	PBKDF2Context(const PBKDF2Config& c, const Hash::ProviderRef& prov)
		: config(c)
		, provider(prov)
	{
	}

	void Update(const unsigned char *data, size_t len) override
	{
		buffer.append(reinterpret_cast<const char *>(data), len);
	}

	std::string Finalize() override
	{
		if (!provider)
			return {}; // No underlying hash (should never happen).

		auto salt = this->GenerateSalt();
		auto hash = PBKDF2(*provider, buffer, salt, config.iterations, config.key_length);
		if (hash.empty())
			return {};

		this->buffer.clear();
		return FMT::format("{}:{}:{}", config.iterations, Base64::Encode(hash), Base64::Encode(salt));
	}
};

class PBKDF2Provider final
	: public Hash::Provider
{
private:
	Hash::ProviderRef provider;

public:
	PBKDF2Config config;

	PBKDF2Provider(Module* mod, const std::string& algorithm)
		: Hash::Provider(mod, FMT::format("pbkdf2-hmac-{}", algorithm))
		, provider(mod, algorithm)
	{
		DisableAutoRegister();
	}

	bool Compare(const std::string& hash, const std::string& plain) override
	{
		if (!provider)
			return false; // No underlying hash (should never happen).

		uint32_t iterations;
		std::string key, salt;
		irc::sepstream stream(hash, ':');
		if (!stream.GetNumericToken(iterations) || !iterations || !stream.GetToken(key) || !stream.GetToken(salt))
			return false; // Malformed hash.

		auto rawkey = Base64::Decode(key);
		auto rawsalt = Base64::Decode(salt);

		auto expected = PBKDF2(*provider, plain, rawsalt, iterations, rawkey.length());
		return !expected.empty() && InspIRCd::TimingSafeCompare(rawkey, expected);
	}

	std::unique_ptr<Hash::Context> CreateContext() override
	{
		return std::make_unique<PBKDF2Context>(config, provider);
	}

	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;
	}
};

class ModuleHashPBKDF2 final
	: public Module
{
private:
	insp::flat_map<std::string, PBKDF2Provider*> algos;
	insp::flat_map<std::string, PBKDF2Config> configs;
	PBKDF2Config defaultconfig;

	void Configure(PBKDF2Provider* algo)
	{
		auto it = configs.find(algo->GetAlgorithm());
		if (it == configs.end())
			algo->config = defaultconfig;
		else
			algo->config = it->second;
	}

public:
	ModuleHashPBKDF2()
		: Module(VF_VENDOR, "Allows other modules to generate PBKDF2 hashes.")
	{
	}

	~ModuleHashPBKDF2() override
	{
		for (const auto &[_, algo] : algos)
			delete algo;
	}

	void init() override
	{
		// Let ourself know about any existing services.
		for (const auto& [_, service] : ServerInstance->Modules.DataProviders)
			OnServiceAdd(*service);
	}


	void ReadConfig(ConfigStatus& status) override
	{
		// Read the config.
		PBKDF2Config newdefaultconfig;
		newdefaultconfig.ReadConfig(ServerInstance->Config->ConfValue("pbkdf2"));

		insp::flat_map<std::string, PBKDF2Config> newconfigs;
		for (const auto& [_, tag] : ServerInstance->Config->ConfTags("pbkdf2prov"))
		{
			const auto hash = tag->getString("hash");
			if (hash.empty())
				throw ModuleException(this, "<pbkdf2prov:name> must not be empty, at {}", tag->source.str());

			PBKDF2Config newprovconfig;
			newprovconfig.ReadConfig(tag, &newdefaultconfig);
			if (!newconfigs.emplace(hash, newprovconfig).second)
				throw ModuleException(this, "<pbkdf2prov:name> must be unique, at {}", tag->source.str());
		}

		// Apply the config.
		std::swap(defaultconfig, newdefaultconfig);
		std::swap(configs, newconfigs);

		// Apply the config to the providers.
		for (const auto &[_, algo] : algos)
			Configure(algo);
	}

	void OnServiceAdd(ServiceProvider& service) override
	{
		if (!service.name.starts_with("hash/"))
			return; //  Not a hash provider.

		auto* hp = static_cast<Hash::Provider*>(&service);
		if (hp->IsKDF())
			return; // Can't use PBKDF2 with a KDF.

		if (algos.find(hp->GetAlgorithm()) != algos.end())
			return; // Already created.

		auto* algo = new PBKDF2Provider(this, hp->GetAlgorithm());
		Configure(algo);
		ServerInstance->Logs.Debug("HASH", "The {} algorithm was added by {}, also adding {}",
			hp->GetAlgorithm(), hp->creator->ModuleFile, algo->GetAlgorithm());

		try
		{
			auto check = checks.find(hp->GetAlgorithm());
			if (check != checks.end())
			{
				algo->Check(check->second);
			}
			else
			{
				ServerInstance->Logs.Debug("HASH", "The {} algorithm lacks runtime checks, unable to verify integrity.",
					algo->GetAlgorithm());
			}
		}
		catch (const ModuleException& err)
		{
			ServerInstance->Logs.Critical("HASH", "{}", err.GetReason());
			delete algo;
			return; // Broken algorithm.
		}

		algos.emplace(hp->GetAlgorithm(), algo);
		ServerInstance->Modules.AddService(*algo);
	}

	void OnServiceDel(ServiceProvider& service) override
	{
		if (!service.name.starts_with("hash/"))
			return; //  Not a hash provider.

		const auto& hp = static_cast<Hash::Provider&>(service);
		auto it = algos.find(hp.GetAlgorithm());
		if (it != algos.end())
		{
			ServerInstance->Logs.Debug("HASH", "The {} algorithm was deleted by {}, also deleting {}",
				hp.GetAlgorithm(), hp.creator->ModuleFile, it->second->GetAlgorithm());
			delete it->second;
			algos.erase(it);
		}
	}

};

MODULE_INIT(ModuleHashPBKDF2)