diff --git a/loader/hash/hash.cpp b/loader/hash/hash.cpp deleted file mode 100644 index e193789d6..000000000 --- a/loader/hash/hash.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "hash.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace geode::prelude; - -static std::string hexEncode(const uint8_t* data, size_t size) { - std::string str; - str.reserve(size * 2); - - constexpr char hexChars[] = "0123456789abcdef"; - - for (size_t i = 0; i < size; ++i) { - str.push_back(hexChars[(data[i] >> 4) & 0x0F]); - str.push_back(hexChars[data[i] & 0x0F]); - } - - return str; -} - -std::string calculateHash(std::span data) { - uint8_t hash[SHA256_DIGEST_LENGTH]; - ::SHA256(data.data(), data.size(), hash); - return hexEncode(hash, sizeof(hash)); -} - -std::string calculateHash(std::string_view data) { - return calculateHash(std::span(reinterpret_cast(data.data()), data.size())); -} - -static Result computeWithReader(auto&& fn) { - auto context = std::unique_ptr(EVP_MD_CTX_new(), &EVP_MD_CTX_free); - if (!context) { - return Err("EVP_MD_CTX_new failed"); - } - - if (EVP_DigestInit_ex(context.get(), EVP_sha256(), nullptr) != 1) { - return Err("EVP_DigestInit_ex failed"); - } - - uint8_t buffer[4096]; - while (true) { - GEODE_UNWRAP_INTO(auto read, fn(buffer, sizeof(buffer))); - if (read == 0) { - break; - } - - if (EVP_DigestUpdate(context.get(), buffer, read) != 1) { - return Err("EVP_DigestUpdate failed"); - } - } - - uint8_t hash[SHA256_DIGEST_LENGTH]; - unsigned int hashLen = 0; - if (EVP_DigestFinal_ex(context.get(), hash, &hashLen) != 1) { - return Err("EVP_DigestFinal_ex failed"); - } - - return Ok(hexEncode(hash, hashLen)); -} - -std::string calculateSHA256(std::filesystem::path const& path) { - std::ifstream file(path, std::ios::binary); - - auto result = computeWithReader([&](auto* buffer, size_t bufSize) -> Result { - if (file.eof()) { - return Ok(0); - } else if (!file.good()) { - return Err("failed to read from file", path); - } - - file.read(reinterpret_cast(buffer), bufSize); - return Ok(file.gcount()); - }); - - if (!result) { - log::error("Failed to compute SHA256 for file '{}': {}", path, result.unwrapErr()); - return ""; - } - - return std::move(result).unwrap(); -} - -std::string calculateSHA256Text(std::filesystem::path const& path) { - std::string input = utils::file::readString(path).unwrapOrDefault(); - StringBuffer<> buf; - - // remove all newlines - for (auto line : asp::iter::lines(input)) { - buf.append(line); - } - - return calculateHash(buf.str()); -} diff --git a/loader/hash/hash.hpp b/loader/hash/hash.hpp index 390d4c140..0e4e6792d 100644 --- a/loader/hash/hash.hpp +++ b/loader/hash/hash.hpp @@ -1,16 +1,20 @@ #pragma once +#include +#include +#include +#include #include #include -#include -std::string calculateSHA256(std::filesystem::path const& path); +inline geode::Sha256 sha256Text(std::filesystem::path const& path) { + std::string input = geode::utils::file::readString(path).unwrapOrDefault(); + geode::utils::StringBuffer<> buf; -std::string calculateSHA256Text(std::filesystem::path const& path); + // remove all newlines + for (auto line : asp::iter::lines(input)) { + buf.append(line); + } -/** - * Calculates the SHA256 hash of the given data, - * used for verifying mods. - */ -std::string calculateHash(std::span data); -std::string calculateHash(std::string_view data); + return geode::sha256(buf.view()); +} diff --git a/loader/include/Geode/utils/hash.hpp b/loader/include/Geode/utils/hash.hpp index 673a4a0a5..a01bd256d 100644 --- a/loader/include/Geode/utils/hash.hpp +++ b/loader/include/Geode/utils/hash.hpp @@ -1,7 +1,13 @@ #pragma once +#include +#include #include #include #include +#include +#include +#include +#include namespace geode { @@ -18,4 +24,73 @@ inline size_t typenameHash() { return hasher(typeid(T).name()); } +template +struct Hash { + std::array data; + + bool operator==(Hash const& other) const noexcept = default; + bool operator!=(Hash const& other) const noexcept = default; + bool operator<(Hash const& other) const noexcept = default; + + /// Returns the hex-encoded string representation of the hash, e.g. "a3b4c5d6e7f8..." + std::string toString() const { + std::string str; + str.resize(N * 2); + this->hexEncode(str.data()); + return str; + } + + /// Writes hex-encoded hash into the given buffer, the buffer must be at least 2 * N bytes long + void hexEncode(void* buf) const { + uint8_t* dest = reinterpret_cast(buf); + constexpr char hexChars[] = "0123456789abcdef"; + + for (size_t i = 0; i < N; ++i) { + dest[2 * i] = hexChars[(data[i] >> 4) & 0x0F]; + dest[2 * i + 1] = hexChars[data[i] & 0x0F]; + } + } +}; + +using Sha256 = Hash<32>; + +/// A class for incrementally calculating the SHA256 hash of some data. +/// Call `update` to advance the state of the hasher with new data, then call `finish` to get the final digest. +class GEODE_DLL Sha256Hasher { +public: + Sha256Hasher(); + ~Sha256Hasher(); + + Sha256Hasher(Sha256Hasher const&) = delete; + Sha256Hasher& operator=(Sha256Hasher const&) = delete; + Sha256Hasher(Sha256Hasher&&) noexcept; + Sha256Hasher& operator=(Sha256Hasher&&) noexcept; + + void update(std::span data); + void update(void const* data, size_t bytes); + void update(std::string_view data); + + Sha256 finish() const; + + /// Resets the internal state of the hash. This allows you to treat the hasher like a new instance, + /// without having to actually create a new object and allocate memory. + void reset(); + +private: + void* m_state = nullptr; +}; + +inline auto format_as(Sha256 const& hash) -> std::string { + return hash.toString(); +} + +/// Reads the binary file at the given path and calculates the SHA256 digest of the contents. +/// This reads the file lazily and uses C++ iostreams. If the performance of that is unacceptable, use `Sha256Hasher` directly. +Result sha256File(std::filesystem::path const& path); + +/// Calculates the SHA256 digest of the given data. +Sha256 sha256(std::span data); +/// Calculates the SHA256 digest of the given data. +Sha256 sha256(std::string_view data); + } diff --git a/loader/src/loader/updater.cpp b/loader/src/loader/updater.cpp index b6fa3a04e..ac31d871e 100644 --- a/loader/src/loader/updater.cpp +++ b/loader/src/loader/updater.cpp @@ -213,7 +213,7 @@ bool updater::verifyLoaderResources() { } // verify hash // if we hash anything other than text, change this - auto hash = calculateSHA256Text(file.path()); + auto hash = sha256Text(file.path()).toString(); const auto& expected = LOADER_RESOURCE_HASHES.at(name); if (hash != expected) { log::debug("Resource hash mismatch: {} ({}, {})", name, hash.substr(0, 7), expected.substr(0, 7)); diff --git a/loader/src/server/DownloadManager.cpp b/loader/src/server/DownloadManager.cpp index fab3c89a2..545f99de7 100644 --- a/loader/src/server/DownloadManager.cpp +++ b/loader/src/server/DownloadManager.cpp @@ -104,7 +104,7 @@ class ModDownload::Impl final { return; } - auto actualHash = ::calculateHash(response.data()); + auto actualHash = geode::sha256(response.data()).toString(); if (actualHash != version.hash) { log::error("Failed to download {}, hash mismatch ({} != {})", m_id, actualHash, version.hash); m_status = DownloadStatusError { diff --git a/loader/src/utils/hash.cpp b/loader/src/utils/hash.cpp new file mode 100644 index 000000000..073feee14 --- /dev/null +++ b/loader/src/utils/hash.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace geode { + +Sha256Hasher::Sha256Hasher() { + // In theory, this should never fail unless out of memory, in which case there's little we can do anyway + m_state = EVP_MD_CTX_new(); + if (!m_state) { + utils::terminate("EVP_MD_CTX_new failed"); + } + + this->reset(); +} + +void Sha256Hasher::reset() { + EVP_DigestInit_ex(static_cast(m_state), EVP_sha256(), nullptr); +} + +Sha256Hasher::~Sha256Hasher() { + EVP_MD_CTX_free(static_cast(m_state)); +} + +Sha256Hasher::Sha256Hasher(Sha256Hasher&& other) noexcept { + *this = std::move(other); +} + +Sha256Hasher& Sha256Hasher::operator=(Sha256Hasher&& other) noexcept { + if (this != &other) { + EVP_MD_CTX_free(static_cast(m_state)); + m_state = other.m_state; + other.m_state = nullptr; + } + return *this; +} + +void Sha256Hasher::update(std::span data) { + update(data.data(), data.size()); +} + +void Sha256Hasher::update(void const* data, size_t bytes) { + EVP_DigestUpdate(static_cast(m_state), data, bytes); +} + +void Sha256Hasher::update(std::string_view data) { + update(data.data(), data.size()); +} + +Sha256 Sha256Hasher::finish() const { + Sha256 hash; + EVP_DigestFinal_ex(static_cast(m_state), hash.data.data(), nullptr); + return hash; +} + +Result sha256File(std::filesystem::path const& path) { + Sha256Hasher hasher; + + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + return Err("Failed to open file '{}' for reading", path); + } + + uint8_t buf[4096]; + while (file.good()) { + file.read(reinterpret_cast(buf), sizeof(buf)); + auto read = file.gcount(); + + if (read > 0) { + hasher.update(buf, read); + } + } + + return Ok(hasher.finish()); +} + +Sha256 sha256(std::span data) { + Sha256Hasher hasher; + hasher.update(data); + return hasher.finish(); +} + +Sha256 sha256(std::string_view data) { + return sha256(std::span{reinterpret_cast(data.data()), data.size()}); +} + +}