diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index d328bc489..d8a67086e 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -244,6 +244,14 @@ endif() target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static) target_link_libraries(liblokinet PRIVATE libunbound) +pkg_check_modules(CRYPT libcrypt IMPORTED_TARGET) +if(CRYPT_FOUND AND NOT CMAKE_CROSSCOMPILING) + add_definitions(-DHAVE_CRYPT) + add_library(libcrypt INTERFACE) + target_link_libraries(libcrypt INTERFACE PkgConfig::CRYPT) + target_link_libraries(liblokinet PRIVATE libcrypt) + message(STATUS "using libcrypt ${CRYPT_VERSION}") +endif() if(BUILD_LIBLOKINET) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 94b5ff15c..5e6a53115 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -317,7 +317,7 @@ namespace llarp ClientOnly, Comment{ "Set the endpoint authentication mechanism.", - "none/whitelist/lmq", + "none/whitelist/lmq/file", }, [this](std::string arg) { if (arg.empty()) @@ -366,6 +366,42 @@ namespace llarp m_AuthWhitelist.emplace(std::move(addr)); }); + conf.defineOption( + "network", + "auth-file", + ClientOnly, + MultiValue, + Comment{ + "Read auth tokens from file to accept endpoint auth", + "Can be provided multiple times", + }, + [this](fs::path arg) { + if (not fs::exists(arg)) + throw std::invalid_argument{ + stringify("cannot load auth file ", arg, " as it does not seem to exist")}; + m_AuthFiles.emplace(std::move(arg)); + }); + conf.defineOption( + "network", + "auth-file-type", + ClientOnly, + Comment{ + "How to interpret the contents of an auth file.", + "Possible values: hashes, plaintext", + }, + [this](std::string arg) { m_AuthFileType = service::ParseAuthFileType(std::move(arg)); }); + + conf.defineOption( + "network", + "auth-static", + ClientOnly, + MultiValue, + Comment{ + "Manually add a static auth code to accept for endpoint auth", + "Can be provided multiple times", + }, + [this](std::string arg) { m_AuthStaticTokens.emplace(std::move(arg)); }); + conf.defineOption( "network", "reachable", diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index ff7ca2e71..09eb1febb 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -115,9 +115,12 @@ namespace llarp std::unordered_map m_mapAddrs; service::AuthType m_AuthType = service::AuthType::eAuthTypeNone; + service::AuthFileType m_AuthFileType = service::AuthFileType::eAuthFileHashes; std::optional m_AuthUrl; std::optional m_AuthMethod; std::unordered_set m_AuthWhitelist; + std::unordered_set m_AuthStaticTokens; + std::set m_AuthFiles; std::vector m_SRVRecords; diff --git a/llarp/crypto/crypto.hpp b/llarp/crypto/crypto.hpp index 56cc6c60f..1af34e6b4 100644 --- a/llarp/crypto/crypto.hpp +++ b/llarp/crypto/crypto.hpp @@ -100,6 +100,10 @@ namespace llarp virtual bool check_identity_privkey(const SecretKey&) = 0; + + /// check if a password hash string matches the challenge + virtual bool + check_passwd_hash(std::string pwhash, std::string challenge) = 0; }; inline Crypto::~Crypto() = default; diff --git a/llarp/crypto/crypto_libsodium.cpp b/llarp/crypto/crypto_libsodium.cpp index 8a14f66af..60f2e5513 100644 --- a/llarp/crypto/crypto_libsodium.cpp +++ b/llarp/crypto/crypto_libsodium.cpp @@ -13,6 +13,11 @@ #include #include #include +#ifdef HAVE_CRYPT +#include +#endif + +#include extern "C" { @@ -463,6 +468,25 @@ namespace llarp auto d = keypair.data(); crypto_kem_keypair(d + PQ_SECRETKEYSIZE, d); } + + bool + CryptoLibSodium::check_passwd_hash(std::string pwhash, std::string challenge) + { + (void)pwhash; + (void)challenge; + bool ret = false; +#ifdef HAVE_CRYPT + auto pos = pwhash.find_last_of('$'); + auto settings = pwhash.substr(0, pos); + crypt_data data{}; + if (char* ptr = crypt_r(challenge.c_str(), settings.c_str(), &data)) + { + ret = ptr == pwhash; + } + sodium_memzero(&data, sizeof(data)); +#endif + return ret; + } } // namespace sodium const byte_t* diff --git a/llarp/crypto/crypto_libsodium.hpp b/llarp/crypto/crypto_libsodium.hpp index f085aee1e..6248e9484 100644 --- a/llarp/crypto/crypto_libsodium.hpp +++ b/llarp/crypto/crypto_libsodium.hpp @@ -104,6 +104,9 @@ namespace llarp bool check_identity_privkey(const SecretKey&) override; + + bool + check_passwd_hash(std::string pwhash, std::string challenge) override; }; } // namespace sodium diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 96f63ee12..d4d76c647 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -174,7 +174,11 @@ namespace llarp LogInfo(Name(), " setting to be not reachable by default"); } - if (conf.m_AuthType != service::AuthType::eAuthTypeNone) + if (conf.m_AuthType == service::AuthType::eAuthTypeFile) + { + m_AuthPolicy = service::MakeFileAuthPolicy(m_router, conf.m_AuthFiles, conf.m_AuthFileType); + } + else if (conf.m_AuthType != service::AuthType::eAuthTypeNone) { std::string url, method; if (conf.m_AuthUrl.has_value() and conf.m_AuthMethod.has_value()) @@ -183,7 +187,12 @@ namespace llarp method = *conf.m_AuthMethod; } auto auth = std::make_shared( - url, method, conf.m_AuthWhitelist, Router()->lmq(), shared_from_this()); + url, + method, + conf.m_AuthWhitelist, + conf.m_AuthStaticTokens, + Router()->lmq(), + shared_from_this()); auth->Start(); m_AuthPolicy = std::move(auth); } diff --git a/llarp/rpc/endpoint_rpc.cpp b/llarp/rpc/endpoint_rpc.cpp index 302f14d65..8fa90bf95 100644 --- a/llarp/rpc/endpoint_rpc.cpp +++ b/llarp/rpc/endpoint_rpc.cpp @@ -6,14 +6,16 @@ namespace llarp::rpc EndpointAuthRPC::EndpointAuthRPC( std::string url, std::string method, - Whitelist_t whitelist, + Whitelist_t whitelist_addrs, + std::unordered_set whitelist_tokens, LMQ_ptr lmq, Endpoint_ptr endpoint) - : m_AuthURL(std::move(url)) - , m_AuthMethod(std::move(method)) - , m_AuthWhitelist(std::move(whitelist)) - , m_LMQ(std::move(lmq)) - , m_Endpoint(std::move(endpoint)) + : m_AuthURL{std::move(url)} + , m_AuthMethod{std::move(method)} + , m_AuthWhitelist{std::move(whitelist_addrs)} + , m_AuthStaticTokens{std::move(whitelist_tokens)} + , m_LMQ{std::move(lmq)} + , m_Endpoint{std::move(endpoint)} {} void @@ -57,13 +59,6 @@ namespace llarp::rpc reply(service::AuthResult{service::AuthResultCode::eAuthAccepted, "explicitly whitelisted"}); return; } - if (not m_Conn.has_value()) - { - // we don't have a connection to the backend so it's failed - reply(service::AuthResult{ - service::AuthResultCode::eAuthFailed, "remote has no connection to auth backend"}); - return; - } if (msg->proto != llarp::service::ProtocolType::Auth) { @@ -72,9 +67,32 @@ namespace llarp::rpc return; } + std::string payload{(char*)msg->payload.data(), msg->payload.size()}; + + if (m_AuthStaticTokens.count(payload)) + { + reply(service::AuthResult{service::AuthResultCode::eAuthAccepted, "explicitly whitelisted"}); + return; + } + + if (not m_Conn.has_value()) + { + if (m_AuthStaticTokens.empty()) + { + // we don't have a connection to the backend so it's failed + reply(service::AuthResult{ + service::AuthResultCode::eAuthFailed, "remote has no connection to auth backend"}); + } + else + { + // static auth mode + reply(service::AuthResult{service::AuthResultCode::eAuthRejected, "access not permitted"}); + } + return; + } + const auto authinfo = msg->EncodeAuthInfo(); std::string_view metainfo{authinfo.data(), authinfo.size()}; - std::string_view payload{(char*)msg->payload.data(), msg->payload.size()}; // call method with 2 parameters: metainfo and userdata m_LMQ->request( *m_Conn, diff --git a/llarp/rpc/endpoint_rpc.hpp b/llarp/rpc/endpoint_rpc.hpp index c84cf6183..02e573d6d 100644 --- a/llarp/rpc/endpoint_rpc.hpp +++ b/llarp/rpc/endpoint_rpc.hpp @@ -20,7 +20,8 @@ namespace llarp::rpc explicit EndpointAuthRPC( std::string url, std::string method, - Whitelist_t whitelist, + Whitelist_t addr_whitelist, + std::unordered_set token_whitelist, LMQ_ptr lmq, Endpoint_ptr endpoint); virtual ~EndpointAuthRPC() = default; @@ -40,6 +41,7 @@ namespace llarp::rpc const std::string m_AuthURL; const std::string m_AuthMethod; const Whitelist_t m_AuthWhitelist; + const std::unordered_set m_AuthStaticTokens; LMQ_ptr m_LMQ; Endpoint_ptr m_Endpoint; std::optional m_Conn; diff --git a/llarp/service/auth.cpp b/llarp/service/auth.cpp index 2f631f3fb..1c6ce91fb 100644 --- a/llarp/service/auth.cpp +++ b/llarp/service/auth.cpp @@ -1,6 +1,11 @@ #include "auth.hpp" #include +#include +#include "protocol.hpp" +#include +#include + namespace llarp::service { /// maybe get auth result from string @@ -22,6 +27,7 @@ namespace llarp::service ParseAuthType(std::string data) { std::unordered_map values = { + {"file", AuthType::eAuthTypeFile}, {"lmq", AuthType::eAuthTypeLMQ}, {"whitelist", AuthType::eAuthTypeWhitelist}, {"none", AuthType::eAuthTypeNone}}; @@ -31,6 +37,25 @@ namespace llarp::service return itr->second; } + AuthFileType + ParseAuthFileType(std::string data) + { + std::unordered_map values = { + {"plain", AuthFileType::eAuthFilePlain}, + {"plaintext", AuthFileType::eAuthFilePlain}, + {"hashed", AuthFileType::eAuthFileHashes}, + {"hashes", AuthFileType::eAuthFileHashes}, + {"hash", AuthFileType::eAuthFileHashes}}; + const auto itr = values.find(data); + if (itr == values.end()) + throw std::invalid_argument("no such auth file type: " + data); +#ifndef HAVE_CRYPT + if (itr->second == AuthFileType::eAuthFileHashes) + throw std::invalid_argument("unsupported auth file type: " + data); +#endif + return itr->second; + } + /// turn an auth result code into an int uint64_t AuthResultCodeAsInt(AuthResultCode code) @@ -58,4 +83,106 @@ namespace llarp::service } } + class FileAuthPolicy : public IAuthPolicy, public std::enable_shared_from_this + { + const std::set m_Files; + const AuthFileType m_Type; + AbstractRouter* const m_Router; + mutable util::Mutex m_Access; + std::unordered_set m_Pending; + /// returns an auth result for a auth info challange, opens every file until it finds a token + /// matching it + /// this is expected to be done in the IO thread + AuthResult + CheckFiles(const AuthInfo& info) const + { + for (const auto& f : m_Files) + { + fs::ifstream i{f}; + std::string line{}; + while (std::getline(i, line)) + { + // split off comments + const auto parts = split_any(line, "#;", true); + if (auto part = parts[0]; not parts.empty() and not parts[0].empty()) + { + // split off whitespaces and check password + if (CheckPasswd(std::string{TrimWhitespace(part)}, info.token)) + return AuthResult{AuthResultCode::eAuthAccepted, "accepted by whitelist"}; + } + } + } + return AuthResult{AuthResultCode::eAuthRejected, "rejected by whitelist"}; + } + + bool + CheckPasswd(std::string hash, std::string challenge) const + { + switch (m_Type) + { + case AuthFileType::eAuthFilePlain: + return hash == challenge; + case AuthFileType::eAuthFileHashes: + return CryptoManager::instance()->check_passwd_hash( + std::move(hash), std::move(challenge)); + default: + return false; + } + } + + public: + FileAuthPolicy(AbstractRouter* r, std::set files, AuthFileType filetype) + : m_Files{std::move(files)}, m_Type{filetype}, m_Router{r} + {} + + void + AuthenticateAsync( + std::shared_ptr msg, std::function hook) override + { + auto reply = m_Router->loop()->make_caller( + [tag = msg->tag, hook, self = shared_from_this()](AuthResult result) { + { + util::Lock _lock{self->m_Access}; + self->m_Pending.erase(tag); + } + hook(result); + }); + { + util::Lock _lock{m_Access}; + m_Pending.emplace(msg->tag); + } + if (msg->proto == ProtocolType::Auth) + { + m_Router->QueueDiskIO( + [self = shared_from_this(), + auth = AuthInfo{std::string{ + reinterpret_cast(msg->payload.data()), msg->payload.size()}}, + reply]() { + try + { + reply(self->CheckFiles(auth)); + } + catch (std::exception& ex) + { + reply(AuthResult{AuthResultCode::eAuthFailed, ex.what()}); + } + }); + } + else + reply(AuthResult{AuthResultCode::eAuthRejected, "protocol error"}); + } + bool + AsyncAuthPending(ConvoTag tag) const override + { + util::Lock _lock{m_Access}; + return m_Pending.count(tag); + } + }; + + std::shared_ptr + MakeFileAuthPolicy(AbstractRouter* r, std::set files, AuthFileType filetype) + { + return std::make_shared(r, std::move(files), filetype); + } + } // namespace llarp::service diff --git a/llarp/service/auth.hpp b/llarp/service/auth.hpp index 98e515e02..b87f9eab7 100644 --- a/llarp/service/auth.hpp +++ b/llarp/service/auth.hpp @@ -44,7 +44,7 @@ namespace llarp::service struct IAuthPolicy { - ~IAuthPolicy() = default; + virtual ~IAuthPolicy() = default; /// asynchronously determine if we accept new convotag from remote service, call hook with /// result later @@ -71,7 +71,16 @@ namespace llarp::service /// manual whitelist eAuthTypeWhitelist, /// LMQ server - eAuthTypeLMQ + eAuthTypeLMQ, + /// static file + eAuthTypeFile, + }; + + /// how to interpret an file for auth + enum class AuthFileType + { + eAuthFilePlain, + eAuthFileHashes, }; /// get an auth type from a string @@ -79,4 +88,13 @@ namespace llarp::service AuthType ParseAuthType(std::string arg); + /// get an auth file type from a string + /// throws std::invalid_argument if arg is invalid + AuthFileType + ParseAuthFileType(std::string arg); + + /// make an IAuthPolicy that reads out of a static file + std::shared_ptr + MakeFileAuthPolicy(AbstractRouter*, std::set files, AuthFileType fileType); + } // namespace llarp::service diff --git a/test/crypto/test_llarp_crypto.cpp b/test/crypto/test_llarp_crypto.cpp index 34747e53d..13105a87e 100644 --- a/test/crypto/test_llarp_crypto.cpp +++ b/test/crypto/test_llarp_crypto.cpp @@ -47,3 +47,48 @@ TEST_CASE("PQ crypto") REQUIRE(c->pqe_decrypt(block, otherShared, pq_keypair_to_secret(keys))); REQUIRE(otherShared == shared); } + +#ifdef HAVE_CRYPT + +TEST_CASE("passwd hash valid") +{ + llarp::sodium::CryptoLibSodium crypto; + + // poggers password hashes + std::set valid_hashes; + // UNIX DES + valid_hashes.emplace("CVu85Ms694POo"); + // sha256 salted + valid_hashes.emplace( + "$5$cIghotiBGjfPC7Fu$" + "TXXxPhpUcEiF9tMnjhEVJFi9AlNDSkNRQFTrXPQTKS9"); + // sha512 salted + valid_hashes.emplace( + "$6$qB77ms3wCIo.xVKP$Hl0RLuDgWNmIW4s." + "5KUbFmnauoTfrWSPJzDCD8ZTSSfwRbMgqgG6F9y3K.YEYVij8g/" + "Js0DRT2RhgXoX0sHGb."); + + for (const auto& hash : valid_hashes) + { + // make sure it is poggers ... + REQUIRE(crypto.check_passwd_hash(hash, "poggers")); + // ... and not inscrutible + REQUIRE(not crypto.check_passwd_hash(hash, "inscrutible")); + } +} + +TEST_CASE("passwd hash malformed") +{ + llarp::sodium::CryptoLibSodium crypto; + + std::set invalid_hashes = { + "stevejobs", + "$JKEDbzgzym1N6", // crypt() for "stevejobs" with a $ at the begining + "$0$zero$AAAAAAAAAAA", + "$$$AAAAAAAAAAAA", + "$LIGMA$BALLS$LMAOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO."}; + for (const auto& hash : invalid_hashes) + REQUIRE(not crypto.check_passwd_hash(hash, "stevejobs")); +} + +#endif