/* * 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 . */ #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> 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(std::ceil(static_cast(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& tag, PBKDF2Config* def = nullptr) { iterations = tag->getNum("iterations", def ? def->iterations : 15'000, 1000); key_length = tag->getNum("length", def ? def->key_length : 32, 1, 1024); salt_length = tag->getNum("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 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(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 CreateContext() override { return std::make_unique(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 algos; insp::flat_map 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 newconfigs; for (const auto& [_, tag] : ServerInstance->Config->ConfTags("pbkdf2prov")) { const auto hash = tag->getString("hash"); if (hash.empty()) throw ModuleException(this, " must not be empty, at {}", tag->source.str()); PBKDF2Config newprovconfig; newprovconfig.ReadConfig(tag, &newdefaultconfig); if (!newconfigs.emplace(hash, newprovconfig).second) throw ModuleException(this, " 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(&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(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)