diff --git a/CMakeLists.txt b/CMakeLists.txt index efb0dde..826e7af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ add_compile_definitions(CHECKPOINT_VERSION="${CHECKPOINT_VERSION}") find_package(PkgConfig REQUIRED) -pkg_check_modules(deps IMPORTED_TARGET openssl sqlite3) +pkg_check_modules(deps IMPORTED_TARGET openssl) target_include_directories(checkpoint PRIVATE diff --git a/html/index.html b/html/index.html index 5e410b9..294cc9a 100644 --- a/html/index.html +++ b/html/index.html @@ -154,7 +154,8 @@ transition: ease-in-out 0.1s; } - @media (pointer:none), (pointer:coarse) { + @media (pointer:none), + (pointer:coarse) { .big-icon { margin-top: 10rem; } @@ -184,7 +185,6 @@ font-size: 1rem; } } -
@@ -198,9 +198,9 @@

Verifying that you are not a bot. This might take a short moment. -
-
- +
+
+ You do not need to do anything.

@@ -219,7 +219,7 @@
- +

Difficulty {{ tl:text challengeDifficulty }}, elapsed 0ms, 0h, 0h/s

@@ -237,6 +237,8 @@ var start = Date.now(); const challengeNonce = "{{ tl:text challengeNonce }}"; const difficulty = parseInt("{{ tl:text challengeDifficulty }}"); + const challengeSig = "{{ tl:text challengeSignature }}"; + const challengeFingerprint = "{{ tl:text challengeFingerprint }}"; function valid(sha) { const MIN_ZEROES = difficulty; @@ -273,8 +275,11 @@ const data = JSON.stringify({ challenge: challengeNonce, solution: it, + difficulty: difficulty, + sig: challengeSig, + fingerprint: challengeFingerprint }); - + fetch("/checkpoint/challenge", { headers: { diff --git a/html/index.min.html b/html/index.min.html index 41c7bd3..dcb6f1f 100644 --- a/html/index.min.html +++ b/html/index.min.html @@ -1,2 +1,2 @@ STOP! - Checkpoint

🛑

STOP!


Verifying that you are not a bot. This might take a short moment.

You do not need to do anything.

Why am I seeing this?


This website protects itself from AI bots and scrapers by asking you to complete a cryptographic challenge before allowing you entry.

Difficulty {{ tl:text challengeDifficulty }}, elapsed 0ms, 0h, 0h/s

Powered by checkpoint v{{ tl:text checkpointVersion }}
- \ No newline at end of file + \ No newline at end of file diff --git a/src/core/Challenge.cpp b/src/core/Challenge.cpp new file mode 100644 index 0000000..11e9662 --- /dev/null +++ b/src/core/Challenge.cpp @@ -0,0 +1,62 @@ +#include "Challenge.hpp" + +#include "Crypto.hpp" + +#include +#include + +constexpr const uint64_t CHALLENGE_VERSION = 1; + +CChallenge::CChallenge(const std::string& fingerprint, const std::string& challenge, int difficulty) : + m_fingerprint(fingerprint), m_challenge(challenge), m_difficulty(difficulty) { + std::string toSign = getSigString(); + + m_sig = g_pCrypto->sign(toSign); + + m_valid = true; +} + +CChallenge::CChallenge(const std::string& jsonResponse) { + auto json = glz::read_json(jsonResponse); + + if (!json.has_value()) + return; + + SChallengeJSON s = json.value(); + + m_challenge = s.challenge; + m_fingerprint = s.fingerprint; + m_sig = s.sig; + + if (!g_pCrypto->verifySignature(getSigString(), m_sig)) + return; + + const auto SHA = g_pCrypto->sha256(m_challenge + std::to_string(s.solution)); + + for (size_t i = 0; i < m_difficulty; ++i) { + if (SHA.at(i) != '0') + return; + } + + m_valid = true; +} + +std::string CChallenge::fingerprint() const { + return m_fingerprint; +} + +std::string CChallenge::challenge() const { + return m_challenge; +} + +std::string CChallenge::signature() const { + return m_sig; +} + +bool CChallenge::valid() const { + return m_valid; +} + +std::string CChallenge::getSigString() { + return fmt::format("{}-{},{}", CHALLENGE_VERSION, m_fingerprint, m_challenge); +} diff --git a/src/core/Challenge.hpp b/src/core/Challenge.hpp new file mode 100644 index 0000000..2cbcf6b --- /dev/null +++ b/src/core/Challenge.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +class CChallenge { + public: + CChallenge(const std::string& fingerprint, const std::string& challenge, int difficulty); + CChallenge(const std::string& jsonResponse); + + std::string fingerprint() const; + std::string challenge() const; + std::string signature() const; + bool valid() const; + + private: + std::string getSigString(); + + std::string m_sig, m_fingerprint, m_challenge; + bool m_valid = false; + int m_difficulty = 4; + + struct SChallengeJSON { + std::string fingerprint, challenge, sig; + int difficulty = 4, solution = 0; + }; +}; \ No newline at end of file diff --git a/src/core/Crypto.cpp b/src/core/Crypto.cpp new file mode 100644 index 0000000..61b40d0 --- /dev/null +++ b/src/core/Crypto.cpp @@ -0,0 +1,200 @@ +#include "Crypto.hpp" + +#include "../GlobalState.hpp" +#include "../config/Config.hpp" +#include "../debug/log.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +constexpr const char* KEY_FILENAME = "privateKey.key"; + +static std::string dataDir() { + static const std::string dir = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.data_dir).string(); + return dir; +} + +static std::string readFileAsText(const std::string& path) { + std::ifstream ifs(path); + auto res = std::string((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); + if (res.back() == '\n') + res.pop_back(); + return res; +} + +CCrypto::CCrypto() { + if (!std::filesystem::exists(dataDir() + "/" + KEY_FILENAME)) { + Debug::log(LOG, "No private key, generating one."); + if (!genKey()) { + Debug::log(CRIT, "Couldn't generate a key."); + throw std::runtime_error("Keygen failed"); + } + } else { + auto f = fopen((dataDir() + "/" + KEY_FILENAME).c_str(), "r"); + PEM_read_PrivateKey(f, &m_evpPkey, nullptr, nullptr); + fclose(f); + } + + if (!m_evpPkey) { + Debug::log(CRIT, "Couldn't read the key."); + throw std::runtime_error("Key read openssl failed"); + } + + Debug::log(LOG, "Read private key"); +} + +CCrypto::~CCrypto() { + if (m_evpPkey) + EVP_PKEY_free(m_evpPkey); +} + +std::vector CCrypto::toByteArr(const std::string_view& s) { + std::vector inAsHash; + inAsHash.reserve(s.size() / 2); + for (size_t i = 0; i < s.size(); i += 2) { + uint8_t byte = std::stoi(std::string{s.substr(i, 2)}, nullptr, 16); + inAsHash.emplace_back(byte); + } + return inAsHash; +} + +std::string CCrypto::sha256(const std::string& in) { + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + if (!ctx) + return ""; + + if (!EVP_DigestInit(ctx, EVP_sha256())) { + EVP_MD_CTX_free(ctx); + return ""; + } + + if (!EVP_DigestUpdate(ctx, in.c_str(), in.size())) { + EVP_MD_CTX_free(ctx); + return ""; + } + + uint8_t buf[32]; + + if (!EVP_DigestFinal(ctx, buf, nullptr)) { + EVP_MD_CTX_free(ctx); + return ""; + } + + std::stringstream ss; + for (size_t i = 0; i < 32; ++i) { + ss << fmt::format("{:02x}", buf[i]); + } + + EVP_MD_CTX_free(ctx); + + return ss.str(); +} + +bool CCrypto::genKey() { + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); + + if (!ctx) + return false; + + if (EVP_PKEY_keygen_init(ctx) <= 0) { + EVP_PKEY_CTX_free(ctx); + return false; + } + + if (EVP_PKEY_keygen(ctx, &m_evpPkey) <= 0) { + EVP_PKEY_CTX_free(ctx); + return false; + } + + auto f = fopen((dataDir() + "/" + KEY_FILENAME).c_str(), "w"); + PEM_write_PrivateKey(f, m_evpPkey, nullptr, nullptr, 0, nullptr, nullptr); + fclose(f); + + EVP_PKEY_CTX_free(ctx); + + return true; +} + +std::string CCrypto::sign(const std::string& in) { + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + if (!ctx) + return ""; + + if (!EVP_DigestSignInit(ctx, nullptr, nullptr, nullptr, m_evpPkey)) { + Debug::log(ERR, "CCrypto::sign: EVP_DigestSignInit: err {}", ERR_error_string(ERR_get_error(), nullptr)); + EVP_MD_CTX_free(ctx); + return ""; + } + + size_t len = 0; + + if (!EVP_DigestSign(ctx, nullptr, &len, (const unsigned char*)in.c_str(), in.size())) { + Debug::log(ERR, "CCrypto::sign: EVP_DigestSign: err {}", ERR_error_string(ERR_get_error(), nullptr)); + EVP_MD_CTX_free(ctx); + return ""; + } + + if (len <= 0) { + EVP_MD_CTX_free(ctx); + return ""; + } + + std::vector buf; + buf.resize(len); + + if (!EVP_DigestSign(ctx, buf.data(), &len, (const unsigned char*)in.c_str(), in.size())) { + Debug::log(ERR, "CCrypto::sign: EVP_DigestSign: err {}", ERR_error_string(ERR_get_error(), nullptr)); + EVP_MD_CTX_free(ctx); + return ""; + } + + std::stringstream ss; + for (size_t i = 0; i < buf.size(); ++i) { + ss << fmt::format("{:02x}", buf[i]); + } + + EVP_MD_CTX_free(ctx); + + return ss.str(); +} + +bool CCrypto::verifySignature(const std::string& in, const std::string& sig) { + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + if (!ctx) + return false; + + if (!EVP_DigestVerifyInit(ctx, nullptr, nullptr, nullptr, m_evpPkey)) { + Debug::log(ERR, "CCrypto::verifySignature: EVP_DigestVerifyInit: err {}", ERR_error_string(ERR_get_error(), nullptr)); + EVP_MD_CTX_free(ctx); + return false; + } + + auto sigAsArr = toByteArr(sig); + + int ret = EVP_DigestVerify(ctx, sigAsArr.data(), sigAsArr.size(), (const unsigned char*)in.c_str(), in.size()); + + if (ret == 1) { + // match + EVP_MD_CTX_free(ctx); + return true; + } + + if (ret == 0) { + // no match + EVP_MD_CTX_free(ctx); + return false; + } + + Debug::log(ERR, "CCrypto::verifySignature: EVP_DigestVerify: err {}", ERR_error_string(ERR_get_error(), nullptr)); + + // invalid sig?? + EVP_MD_CTX_free(ctx); + return false; +} diff --git a/src/core/Crypto.hpp b/src/core/Crypto.hpp new file mode 100644 index 0000000..a94ac84 --- /dev/null +++ b/src/core/Crypto.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include +#include + +class CCrypto { + public: + CCrypto(); + ~CCrypto(); + + std::string sha256(const std::string& in); + std::string sign(const std::string& in); + bool verifySignature(const std::string& in, const std::string& sig); + + private: + EVP_PKEY* m_evpPkey = nullptr; + + bool genKey(); + void readKey(); + std::vector toByteArr(const std::string_view& s); +}; + +inline std::unique_ptr g_pCrypto; \ No newline at end of file diff --git a/src/core/Db.cpp b/src/core/Db.cpp deleted file mode 100644 index 377d945..0000000 --- a/src/core/Db.cpp +++ /dev/null @@ -1,276 +0,0 @@ -#include "Db.hpp" - -#include "../GlobalState.hpp" -#include "../debug/log.hpp" -#include "../config/Config.hpp" - -#include -#include -#include -#include - -constexpr const char* DB_FILE = "data.db"; -constexpr const uint64_t DB_TIME_BEFORE_CLEANUP_MS = 1000 * 60 * 10; // 10 mins -constexpr const uint64_t DB_TOKEN_LIFE_LENGTH_S = 60 * 60; // 1hr -constexpr const uint64_t DB_CHALLENGE_LIFE_LENGTH_S = 60 * 10; // 10 mins -constexpr const uint64_t DB_SCHEMA_VERSION = 2; - -// -static std::string readFileAsText(const std::string& path) { - std::ifstream ifs(path); - auto res = std::string((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); - if (res.back() == '\n') - res.pop_back(); - return res; -} - -static std::string dbDir() { - static const std::string dir = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.data_dir).string(); - return dir; -} - -static std::string dbPath() { - static const std::string path = dbDir() + "/" + DB_FILE; - return path; -} - -static bool isHashValid(const std::string_view sv) { - return std::all_of(sv.begin(), sv.end(), [](const char& c) { return (c >= 'a' && c <= 'f') || std::isdigit(c); }); -} - -CDatabase::CDatabase() { - if (!std::filesystem::exists(dbDir())) { - Debug::log(LOG, "Data dir doesn't exist, creating."); - std::filesystem::create_directory(dbDir()); - } - - if (std::filesystem::exists(dbPath())) { - if (std::filesystem::exists(dbDir() + "/schema")) { - int schema = std::stoi(readFileAsText(dbDir() + "/schema")); - if (schema == DB_SCHEMA_VERSION) { - if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK) - throw std::runtime_error("failed to open sqlite3 db"); - - cleanupDb(); - return; - } else - Debug::log(LOG, "Database outdated, recreating db"); - } else - Debug::log(LOG, "Database schema not present, recreating db"); - } else - Debug::log(LOG, "Database not present, creating one"); - - std::filesystem::remove(dbPath()); - - if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK) - throw std::runtime_error("failed to open sqlite3 db"); - - std::ofstream of(dbDir() + "/schema", std::ios::trunc); - of << DB_SCHEMA_VERSION; - of.close(); - - // create db layout - char* errmsg = nullptr; - - const char* CHALLENGE_TABLE = R"#( -CREATE TABLE challenges ( - nonce TEXT NOT NULL, - fingerprint TEXT NOT NULL, - difficulty INTEGER NOT NULL, - epoch INTEGER NOT NULL, - CONSTRAINT PK PRIMARY KEY (nonce) -);)#"; - - sqlite3_exec(m_db, CHALLENGE_TABLE, [](void* data, int len, char** a, char** b) -> int { return 0; }, nullptr, &errmsg); - - const char* TOKENS_TABLE = R"#( -CREATE TABLE tokens ( - token TEXT NOT NULL, - fingerprint TEXT NOT NULL, - epoch INTEGER NOT NULL, - CONSTRAINT PK PRIMARY KEY (token) -);)#"; - - sqlite3_exec(m_db, TOKENS_TABLE, [](void* data, int len, char** a, char** b) -> int { return 0; }, nullptr, &errmsg); -} - -CDatabase::~CDatabase() { - if (m_db) - sqlite3_close(m_db); -} - -void CDatabase::addChallenge(const SDatabaseChallengeEntry& entry) { - if (!isHashValid(entry.nonce)) - return; - - if (!isHashValid(entry.fingerprint)) - return; - - const std::string CMD = fmt::format(R"#( -INSERT INTO challenges VALUES ( -"{}", "{}", {}, {} -);)#", - entry.nonce, entry.fingerprint, entry.difficulty, entry.epoch); - - char* errmsg = nullptr; - sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); - - if (errmsg) - Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); -} - -std::optional CDatabase::getChallenge(const std::string& nonce) { - if (!isHashValid(nonce)) - return std::nullopt; - - const std::string CMD = fmt::format(R"#( -SELECT * FROM challenges WHERE nonce = "{}"; -)#", - nonce); - - char* errmsg = nullptr; - CDatabase::SQueryResult result; - - sqlite3_exec( - m_db, CMD.c_str(), - [](void* result, int len, char** a, char** b) -> int { - auto res = reinterpret_cast(result); - - for (size_t i = 0; i < len; ++i) { - res->result.push_back(a[i]); - res->result2.push_back(b[i]); - } - - return 0; - }, - &result, &errmsg); - - if (errmsg || result.result.size() < 4) - return std::nullopt; - - return SDatabaseChallengeEntry{.nonce = nonce, .difficulty = std::stoi(result.result.at(2)), .epoch = std::stoull(result.result.at(3)), .fingerprint = result.result.at(1)}; -} - -void CDatabase::dropChallenge(const std::string& nonce) { - if (!isHashValid(nonce)) - return; - - const std::string CMD = fmt::format(R"#( -DELETE FROM tokens WHERE token = "{}" -)#", - nonce); - - char* errmsg = nullptr; - sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); - - if (errmsg) - Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); -} - -void CDatabase::addToken(const SDatabaseTokenEntry& entry) { - if (!isHashValid(entry.token)) - return; - - if (!isHashValid(entry.fingerprint)) - return; - - const std::string CMD = fmt::format(R"#( -INSERT INTO tokens VALUES ( -"{}", "{}", {} -);)#", - entry.token, entry.fingerprint, entry.epoch); - - char* errmsg = nullptr; - sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); - - if (errmsg) - Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); -} - -void CDatabase::dropToken(const std::string& token) { - if (!isHashValid(token)) - return; - - const std::string CMD = fmt::format(R"#( -DELETE FROM tokens WHERE token = "{}" -)#", - token); - - char* errmsg = nullptr; - sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); - - if (errmsg) - Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); -} - -std::optional CDatabase::getToken(const std::string& token) { - if (!isHashValid(token)) - return std::nullopt; - - if (shouldCleanupDb()) - cleanupDb(); - - const std::string CMD = fmt::format(R"#( -SELECT * FROM tokens WHERE token = "{}"; -)#", - token); - - char* errmsg = nullptr; - CDatabase::SQueryResult result; - - sqlite3_exec( - m_db, CMD.c_str(), - [](void* result, int len, char** a, char** b) -> int { - auto res = reinterpret_cast(result); - - for (size_t i = 0; i < len; ++i) { - res->result.push_back(a[i]); - res->result2.push_back(b[i]); - } - - return 0; - }, - &result, &errmsg); - - if (errmsg || result.result.size() < 3) - return std::nullopt; - - return SDatabaseTokenEntry{.token = token, .epoch = std::stoull(result.result.at(2)), .fingerprint = result.result.at(1)}; -} - -bool CDatabase::shouldCleanupDb() { - const auto TIME = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); - const auto LAST = std::chrono::duration_cast(m_lastDbCleanup.time_since_epoch()).count(); - - if (TIME - LAST > DB_TIME_BEFORE_CLEANUP_MS) - return true; - - return false; -} - -void CDatabase::cleanupDb() { - m_lastDbCleanup = std::chrono::steady_clock::now(); - - const auto TIME = std::chrono::milliseconds(std::time(nullptr)).count(); - - std::string CMD = fmt::format(R"#( -DELETE FROM tokens WHERE epoch < {}; -)#", - TIME - DB_TOKEN_LIFE_LENGTH_S); - - char* errmsg = nullptr; - sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); - - if (errmsg) - Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); - - CMD = fmt::format(R"#( -DELETE FROM challenges WHERE epoch < {}; -)#", - TIME - DB_CHALLENGE_LIFE_LENGTH_S); - - sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); - - if (errmsg) - Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); -} diff --git a/src/core/Db.hpp b/src/core/Db.hpp deleted file mode 100644 index e8a33ef..0000000 --- a/src/core/Db.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -struct SDatabaseChallengeEntry { - std::string nonce = ""; - int difficulty = 0; - unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); - std::string fingerprint = ""; -}; - -struct SDatabaseTokenEntry { - std::string token = ""; - unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); - std::string fingerprint = ""; -}; - -class CDatabase { - public: - CDatabase(); - ~CDatabase(); - - void addChallenge(const SDatabaseChallengeEntry& entry); - std::optional getChallenge(const std::string& nonce); - void dropChallenge(const std::string& nonce); - - void addToken(const SDatabaseTokenEntry& entry); - std::optional getToken(const std::string& token); - void dropToken(const std::string& token); - - private: - struct SQueryResult { - bool failed = false; - std::string error = ""; - std::vector result; - std::vector result2; - }; - - sqlite3* m_db = nullptr; - std::chrono::steady_clock::time_point m_lastDbCleanup = std::chrono::steady_clock::now(); - - void cleanupDb(); - bool shouldCleanupDb(); -}; - -inline std::unique_ptr g_pDB; \ No newline at end of file diff --git a/src/core/Handler.cpp b/src/core/Handler.cpp index 01f8377..b229b96 100644 --- a/src/core/Handler.cpp +++ b/src/core/Handler.cpp @@ -1,4 +1,7 @@ #include "Handler.hpp" +#include "Crypto.hpp" +#include "Token.hpp" +#include "Challenge.hpp" #include "../headers/authorization.hpp" #include "../headers/cfHeader.hpp" #include "../headers/xforwardfor.hpp" @@ -8,7 +11,6 @@ #include "../debug/log.hpp" #include "../GlobalState.hpp" #include "../config/Config.hpp" -#include "Db.hpp" #include #include @@ -20,7 +22,8 @@ #include #include -constexpr const uint64_t TOKEN_MAX_AGE_MS = 1000 * 60 * 60; // 1hr +constexpr const uint64_t TOKEN_MAX_AGE_MS = 1000 * 60 * 60; // 1hr +constexpr const char* TOKEN_COOKIE_NAME = "checkpoint-token"; // static std::string readFileAsText(const std::string& path) { @@ -57,36 +60,6 @@ static std::string generateToken() { return ss.str(); } -static std::string sha256(const std::string& string) { - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); - if (!ctx) - return ""; - - if (!EVP_DigestInit(ctx, EVP_sha256())) { - EVP_MD_CTX_free(ctx); - return ""; - } - - if (!EVP_DigestUpdate(ctx, string.c_str(), string.size())) { - EVP_MD_CTX_free(ctx); - return ""; - } - - uint8_t buf[32]; - - if (!EVP_DigestFinal(ctx, buf, nullptr)) { - EVP_MD_CTX_free(ctx); - return ""; - } - - std::stringstream ss; - for (size_t i = 0; i < 32; ++i) { - ss << fmt::format("{:02x}", buf[i]); - } - - return ss.str(); -} - std::string CServerHandler::fingerprintForRequest(const Pistache::Http::Request& req) { const auto HEADERS = req.headers(); std::shared_ptr acceptEncodingHeader; @@ -132,7 +105,7 @@ std::string CServerHandler::fingerprintForRequest(const Pistache::Http::Request& input += req.address().host(); - return sha256(input); + return g_pCrypto->sha256(input); } bool CServerHandler::isResourceCheckpoint(const std::string_view& res) { @@ -247,24 +220,24 @@ void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Htt } } - if (req.cookies().has("CheckpointToken")) { + if (req.cookies().has(TOKEN_COOKIE_NAME)) { // check the token - const auto TOKEN = g_pDB->getToken(req.cookies().get("CheckpointToken").value); - if (TOKEN) { - const auto AGE = std::chrono::milliseconds(std::time(nullptr)).count() - TOKEN->epoch; - if (AGE <= TOKEN_MAX_AGE_MS && TOKEN->fingerprint == fingerprintForRequest(req)) { + const auto TOKEN = CToken(req.cookies().get(TOKEN_COOKIE_NAME).value); + if (TOKEN.valid()) { + const auto AGE = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count() - + std::chrono::duration_cast(TOKEN.issued().time_since_epoch()).count(); + if (AGE <= TOKEN_MAX_AGE_MS && TOKEN.fingerprint() == fingerprintForRequest(req)) { Debug::log(LOG, " | Action: PASS (token)"); proxyPass(req, response); return; } else { // token has been used from a different IP or is expired. Nuke it. - g_pDB->dropToken(TOKEN->token); if (AGE > TOKEN_MAX_AGE_MS) Debug::log(LOG, " | Action: CHALLENGE (token expired)"); else Debug::log(LOG, " | Action: CHALLENGE (token fingerprint mismatch)"); } } else - Debug::log(LOG, " | Action: CHALLENGE (token not found in db)"); + Debug::log(LOG, " | Action: CHALLENGE (token invalid)"); } else Debug::log(LOG, " | Action: CHALLENGE (no token)"); @@ -279,64 +252,20 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist const auto JSON = req.body(); const auto FINGERPRINT = fingerprintForRequest(req); - std::shared_ptr cfHeader; - try { - cfHeader = Pistache::Http::Header::header_cast(req.headers().get("cf-connecting-ip")); - } catch (std::exception& e) { - ; // silent ignore - } + const auto CHALLENGE = CChallenge(req.body()); - auto json = glz::read_json(JSON); - STokenResponse resp; - - if (!json) { - resp.error = "bad input"; - response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); + if (!CHALLENGE.valid()) { + response.send(Pistache::Http::Code::Bad_Request, "Bad request"); return; } - auto val = json.value(); - - const auto CHALLENGE = g_pDB->getChallenge(val.challenge); - - if (!CHALLENGE.has_value()) { - resp.error = "bad challenge"; - response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); - return; - } - - if (CHALLENGE->fingerprint != FINGERPRINT) { - resp.error = "bad challenge"; - response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); - return; - } - - // drop challenge already. - g_pDB->dropChallenge(val.challenge); - - // verify challenge - const auto SHA = sha256(val.challenge + std::to_string(val.solution)); - - for (int i = 0; i < CHALLENGE->difficulty; ++i) { - if (SHA.at(i) != '0') { - resp.error = "bad solution"; - response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); - return; - } - } - // correct solution, return a token - const auto TOKEN = generateToken(); + const auto TOKEN = CToken(FINGERPRINT, std::chrono::system_clock::now()); - g_pDB->addToken(SDatabaseTokenEntry{.token = TOKEN, .fingerprint = FINGERPRINT}); + response.headers().add(std::make_shared(std::string{TOKEN_COOKIE_NAME} + "=" + TOKEN.tokenCookie() + "; HttpOnly; Path=/; Secure; SameSite=Lax")); - resp.success = true; - resp.token = TOKEN; - - response.headers().add(std::make_shared("CheckpointToken=" + TOKEN + "; HttpOnly; Path=/; Secure; SameSite=Lax")); - - response.send(Pistache::Http::Code::Ok, glz::write_json(resp).value()); + response.send(Pistache::Http::Code::Ok, "Ok"); } void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) { @@ -348,10 +277,12 @@ void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Htt const auto NONCE = generateNonce(); const auto DIFFICULTY = 4; - g_pDB->addChallenge(SDatabaseChallengeEntry{.nonce = NONCE, .difficulty = DIFFICULTY, .fingerprint = fingerprintForRequest(req)}); + const auto CHALLENGE = CChallenge(fingerprintForRequest(req), NONCE, DIFFICULTY); page.add("challengeDifficulty", CTinylatesProp(std::to_string(DIFFICULTY))); page.add("challengeNonce", CTinylatesProp(NONCE)); + page.add("challengeSignature", CTinylatesProp(CHALLENGE.signature())); + page.add("challengeFingerprint", CTinylatesProp(CHALLENGE.fingerprint())); page.add("checkpointVersion", CTinylatesProp(CHECKPOINT_VERSION)); response.send(Pistache::Http::Code::Ok, page.render().value_or("error")); } diff --git a/src/core/Token.cpp b/src/core/Token.cpp new file mode 100644 index 0000000..56d3f05 --- /dev/null +++ b/src/core/Token.cpp @@ -0,0 +1,69 @@ +#include "Token.hpp" + +#include "Crypto.hpp" + +#include + +constexpr const uint64_t TOKEN_VERSION = 1; + +CToken::CToken(const std::string& fingerprint, std::chrono::system_clock::time_point issued) : m_fingerprint(fingerprint), m_issued(issued) { + std::string toSign = getSigString(); + + m_sig = g_pCrypto->sign(toSign); + + m_fullCookie = fmt::format("{},{}", toSign, m_sig); + + m_valid = true; +} + +CToken::CToken(const std::string& cookie) : m_fullCookie(cookie) { + // try to parse the cookie + if (std::count(cookie.begin(), cookie.end(), ',') != 2) + return; + + if (!cookie.contains('-')) + return; + + auto dash = cookie.find('-'); + + try { + if (std::stoi(cookie.substr(0, dash)) != TOKEN_VERSION) + return; + } catch (std::exception& e) { return; } + + std::string_view cookieData = std::string_view{cookie}.substr(dash + 1); + auto firstComma = cookieData.find(','); + auto lastComma = cookieData.find_last_of(','); + + m_fingerprint = cookieData.substr(0, firstComma); + m_sig = cookieData.substr(lastComma + 1); + const auto tpStrMs = cookieData.substr(firstComma + 1, lastComma - firstComma - 1); + + try { + m_issued = std::chrono::system_clock::time_point(std::chrono::milliseconds(std::stoull(std::string{tpStrMs}))); + } catch (std::exception& e) { return; } + + std::string toSign = getSigString(); + + m_valid = g_pCrypto->verifySignature(toSign, m_sig); +} + +std::string CToken::tokenCookie() const { + return m_fullCookie; +} + +std::string CToken::fingerprint() const { + return m_fingerprint; +} + +bool CToken::valid() const { + return m_valid; +} + +std::chrono::system_clock::time_point CToken::issued() const { + return m_issued; +} + +std::string CToken::getSigString() { + return fmt::format("{}-{},{}", TOKEN_VERSION, m_fingerprint, std::chrono::duration_cast(m_issued.time_since_epoch()).count()); +} diff --git a/src/core/Token.hpp b/src/core/Token.hpp new file mode 100644 index 0000000..e28a8c3 --- /dev/null +++ b/src/core/Token.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +class CToken { + public: + CToken(const std::string& fingerprint, std::chrono::system_clock::time_point issued); + CToken(const std::string& cookie); + + std::string tokenCookie() const; + std::string fingerprint() const; + bool valid() const; + std::chrono::system_clock::time_point issued() const; + + private: + std::string getSigString(); + + std::string m_sig, m_fingerprint, m_fullCookie; + std::chrono::system_clock::time_point m_issued; + bool m_valid = false; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 7397035..74e1fa0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,7 +18,7 @@ #include "debug/log.hpp" #include "core/Handler.hpp" -#include "core/Db.hpp" +#include "core/Crypto.hpp" #include "config/Config.hpp" @@ -79,7 +79,7 @@ int main(int argc, char** argv, char** envp) { Pistache::Http::Header::Registry::instance().registerHeader(); Pistache::Http::Header::Registry::instance().registerHeader(); - g_pDB = std::make_unique(); + g_pCrypto = std::make_unique(); auto endpoint = std::make_unique(address); auto opts = Pistache::Http::Endpoint::options().threads(threads).flags(Pistache::Tcp::Options::ReuseAddr | Pistache::Tcp::Options::ReusePort);