// 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 "sha2/sha2.c"

#include "module.h"
#include "modules/encryption.h"

template <typename SHAContext,
	void (* SHAInit)(SHAContext *),
	void (* SHAUpdate)(SHAContext *, const unsigned char *, unsigned int),
	void (* SHAFinal)(SHAContext *, unsigned char *)>
class SHA2Context final
	: public Encryption::Context
{
private:
	SHAContext context;
	const size_t digest_size;

public:
	SHA2Context(size_t ds)
		: digest_size(ds)
	{
		SHAInit(&context);
	}

	void Update(const unsigned char *data, size_t len) override
	{
		SHAUpdate(&context, data, len);
	}

	Anope::string Finalize() override
	{
		std::vector<unsigned char> digest(digest_size);
		SHAFinal(&context, digest.data());
		return Anope::string(reinterpret_cast<const char *>(digest.data()), digest.size());
	}
};

template <typename SHAContext,
	void (* SHAInit)(SHAContext *),
	void (* SHAUpdate)(SHAContext *, const unsigned char *, unsigned int),
	void (* SHAFinal)(SHAContext *, unsigned char *)>
class SHA2Provider final
	: public Encryption::Provider
{
public:
	SHA2Provider(Module *creator, const Anope::string &algorithm, size_t bs, size_t ds)
		: Encryption::Provider(creator, algorithm, bs, ds)
	{
	}

	std::unique_ptr<Encryption::Context> CreateContext() override
	{
		return std::make_unique<SHA2Context<SHAContext, SHAInit, SHAUpdate, SHAFinal>>(this->digest_size);
	}
};

class ESHA2 final
	: public Module
{
private:
	Encryption::Provider *defaultprovider = nullptr;
	SHA2Provider<sha224_ctx, sha224_init, sha224_update, sha224_final> sha224provider;
	SHA2Provider<sha256_ctx, sha256_init, sha256_update, sha256_final> sha256provider;
	SHA2Provider<sha384_ctx, sha384_init, sha384_update, sha384_final> sha384provider;
	SHA2Provider<sha512_ctx, sha512_init, sha512_update, sha512_final> sha512provider;

	Anope::string GenerateKey(size_t keylen)
	{
		static std::random_device device;
		static std::mt19937 engine(device());
		static std::uniform_int_distribution<int> dist(CHAR_MIN, CHAR_MAX);
		Anope::string keybuf(keylen, ' ');
		for (size_t i = 0; i < keylen; ++i)
			keybuf[i] = static_cast<char>(dist(engine));
		return keybuf;
	}

	Encryption::Provider *GetAlgorithm(const Anope::string &algorithm)
	{
		if (algorithm == "sha224")
			return &sha224provider;
		if (algorithm == "sha256")
			return &sha256provider;
		if (algorithm == "sha384")
			return &sha384provider;
		if (algorithm == "sha512")
			return &sha512provider;
		return nullptr;
	}

public:
	ESHA2(const Anope::string &modname, const Anope::string &creator)
		: Module(modname, creator, ENCRYPTION | VENDOR)
		, sha224provider(this, "sha224", SHA224_BLOCK_SIZE, SHA224_DIGEST_SIZE)
		, sha256provider(this, "sha256", SHA256_BLOCK_SIZE, SHA256_DIGEST_SIZE)
		, sha384provider(this, "sha384", SHA384_BLOCK_SIZE, SHA384_DIGEST_SIZE)
		, sha512provider(this, "sha512", SHA512_BLOCK_SIZE, SHA512_DIGEST_SIZE)
	{
		sha224provider.Check({
			{ "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", "" },
			{ "730e109bd7a8a32b1cb9d9a09aa2325d2430587ddbc0c38bad911525", "The quick brown fox jumps over the lazy dog" },
		});
		sha256provider.Check({
			{ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "" },
			{ "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", "The quick brown fox jumps over the lazy dog" },
		});
		sha384provider.Check({
			{ "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", "" },
			{ "ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1", "The quick brown fox jumps over the lazy dog" },
		});
		sha512provider.Check({
			{ "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", "" },
			{ "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", "The quick brown fox jumps over the lazy dog" },
		});
	}

	void OnReload(Configuration::Conf &conf) override
	{
		this->defaultprovider = GetAlgorithm(Config->GetModule(this).Get<const Anope::string>("algorithm", "sha512"));
	}

	EventReturn OnEncrypt(const Anope::string &src, Anope::string &dest) override
	{
		if (!defaultprovider)
			return EVENT_CONTINUE;

		auto key = GenerateKey(defaultprovider->digest_size);
		auto hmac = defaultprovider->HMAC(key, src);
		auto enc = "hmac-" + defaultprovider->name + ":" + Anope::Hex(hmac) + ":" + Anope::Hex(key);
		Log(LOG_DEBUG_2) << "(enc_sha2) hashed password from [" << src << "] to [" << enc << "]";
		dest = enc;
		return EVENT_ALLOW;
	}

	void OnCheckAuthentication(User *, IdentifyRequest *req) override
	{
		auto *na = NickAlias::Find(req->GetAccount());
		if (!na)
			return;

		NickCore *nc = na->nc;
		auto apos = nc->pass.find(':');
		if (apos == Anope::string::npos)
			return;

		Anope::string hash_method(nc->pass.begin(), nc->pass.begin() + apos);
		bool is_hmac = !hash_method.compare(0, 5, "hmac-", 5);
		if (!is_hmac && hash_method.compare(0, 4, "raw-", 4))
			return; // Not a SHA-2 password.

		auto *provider = GetAlgorithm(hash_method.substr(is_hmac ? 5 : 4));
		if (!provider)
			return; // Not a hash for this module.

		auto valid = false;
		if (is_hmac)
		{
			auto bpos = nc->pass.find(':', apos + 1);
			if (bpos == Anope::string::npos)
				return; // No HMAC key.

			// If you are using PHP you can validate passwords like this:
			//
			// <?php
			// function check_pass($user_pass, $anope_pass) {
			// 	[$algo, $hash, $key] = explode(':', $anope_pass);
			// 	$hash_algo           = substr($algo, 5);
			// 	$unhex_key           = hex2bin($key);
			// 	$user_hash           = hash_hmac($hash_algo, $user_pass, $unhex_key);
			// 	return hash_equals($hash, $user_hash);
			// }
			//
			// $anope_pass  = "hmac-sha256:5f7f039818f6e10be84dab0f49610a387d3818b2c883eacdc1778c66a7cecf3b:1a924a3b05a11d7bf3c752d820391dbe01fb7e5d83f7400765de987c3fbb0ec3";
			// var_dump(check_pass("test12345", $anope_pass));     // valid
			// var_dump(check_pass("test123456789", $anope_pass)); // invalid

			Anope::string pass_hex(nc->pass.begin() + apos + 1, nc->pass.begin() + bpos);
			Anope::string key_hex(nc->pass.begin() + bpos + 1, nc->pass.end());
			Anope::string key;
			Anope::Unhex(key_hex, key);

			auto enc = Anope::Hex(provider->HMAC(key, req->GetPassword()));
			valid = pass_hex.equals_cs(enc);
		}
		else
		{
			Anope::string pass_hex(nc->pass.begin() + apos + 1, nc->pass.end());
			valid = provider->Compare(pass_hex, req->GetPassword());
		}

		if (valid)
		{
			// If we are NOT the first encryption module, the password is a raw
			// hash, or the algorithm is different we want to re-encrypt the
			// password with the primary encryption method.
			if (ModuleManager::FindFirstOf(ENCRYPTION) != this || !is_hmac || provider != defaultprovider)
				Anope::Encrypt(req->GetPassword(), nc->pass);
			req->Success(this, na);
		}
	}
};

MODULE_INIT(ESHA2)
