See More

/** * Authentication utilities implementation */ #include "auth.h" #include "config.h" #include "lang.h" #include "user_store.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ragger { namespace fs = std::filesystem; // Base64url encoding (RFC 4648 section 5) static std::string base64url_encode(const unsigned char* data, size_t len) { static const char* base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; std::string result; result.reserve((len * 4 / 3) + 3); for (size_t i = 0; i < len; ) { uint32_t octet_a = i < len ? data[i++] : 0; uint32_t octet_b = i < len ? data[i++] : 0; uint32_t octet_c = i < len ? data[i++] : 0; uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c; result.push_back(base64_chars[(triple >> 18) & 0x3F]); result.push_back(base64_chars[(triple >> 12) & 0x3F]); result.push_back(base64_chars[(triple >> 6) & 0x3F]); result.push_back(base64_chars[triple & 0x3F]); } // Remove padding (base64url doesn't use padding) size_t padding = (3 - (len % 3)) % 3; if (padding > 0) { result.resize(result.size() - padding); } return result; } std::string hash_token(const std::string& token) { unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256(reinterpret_cast(token.c_str()), token.size(), hash); // Convert to hex string std::ostringstream oss; for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) { oss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return oss.str(); } std::string token_path() { return config().resolved_token_path(); } std::string load_token() { std::string path = token_path(); std::ifstream f(path); if (!f.is_open()) return ""; std::string token; std::getline(f, token); // Trim whitespace size_t start = token.find_first_not_of(" \t\r\n"); size_t end = token.find_last_not_of(" \t\r\n"); if (start == std::string::npos) return ""; return token.substr(start, end - start + 1); } std::string generate_token() { unsigned char random_bytes[32]; // Read from /dev/urandom FILE* f = fopen("/dev/urandom", "rb"); if (!f) { throw std::runtime_error(lang::ERR_URANDOM_OPEN); } size_t read_count = fread(random_bytes, 1, 32, f); fclose(f); if (read_count != 32) { throw std::runtime_error(lang::ERR_URANDOM_READ); } return base64url_encode(random_bytes, 32); } std::string ensure_token() { std::string path = token_path(); // Check if token file exists if (fs::exists(path)) { std::string token = load_token(); if (!token.empty()) return token; } // Create parent directory fs::create_directories(fs::path(path).parent_path()); // Generate new token std::string token = generate_token(); // Write to file std::ofstream f(path); if (!f.is_open()) { throw std::runtime_error(std::format(lang::ERR_TOKEN_WRITE, path)); } f << token << std::endl; f.close(); // Set permissions to 0640 (owner read/write + group read) // Group read allows the daemon (_ragger) to read user tokens chmod(path.c_str(), 0640); return token; } // NOTE: The old time-seeded token rotation system (server.secret + // token_for_minute + 24h verify window) has been removed. Tokens are now // plain random values: generate_token() → hash_token() → stored in DB. // The framework (users.token_hash, issue_token, verify_token) is kept for the // planned labeled-token system (see GitHub issue: "Labeled API tokens"). // PBKDF2 password hashing static const int PBKDF2_ITERATIONS = 600000; static const int PBKDF2_SALT_LEN = 16; static const int PBKDF2_KEY_LEN = 32; static std::string bytes_to_hex(const unsigned char* data, size_t len) { std::ostringstream oss; for (size_t i = 0; i < len; ++i) { oss << std::hex << std::setw(2) << std::setfill('0') << static_cast(data[i]); } return oss.str(); } static std::vector hex_to_bytes(const std::string& hex) { std::vector bytes; for (size_t i = 0; i + 1 < hex.size(); i += 2) { unsigned int byte; std::sscanf(hex.c_str() + i, "%02x", &byte); bytes.push_back(static_cast(byte)); } return bytes; } std::string generate_random_token(int bytes) { std::vector buf(bytes); if (RAND_bytes(buf.data(), bytes) != 1) { throw std::runtime_error(lang::ERR_RANDOM_BYTES); } // URL-safe base64-like: hex encoding (simple, always works) return bytes_to_hex(buf.data(), bytes); } std::string hash_password(const std::string& password) { unsigned char salt[PBKDF2_SALT_LEN]; if (RAND_bytes(salt, PBKDF2_SALT_LEN) != 1) { throw std::runtime_error(lang::ERR_RANDOM_SALT); } unsigned char key[PBKDF2_KEY_LEN]; if (PKCS5_PBKDF2_HMAC( password.c_str(), static_cast(password.size()), salt, PBKDF2_SALT_LEN, PBKDF2_ITERATIONS, EVP_sha256(), PBKDF2_KEY_LEN, key) != 1) { throw std::runtime_error(lang::ERR_PBKDF2); } // Format: pbkdf2:iterations:salt_hex:key_hex return "pbkdf2:" + std::to_string(PBKDF2_ITERATIONS) + ":" + bytes_to_hex(salt, PBKDF2_SALT_LEN) + ":" + bytes_to_hex(key, PBKDF2_KEY_LEN); } bool verify_password(const std::string& password, const std::string& stored_hash) { // Parse "pbkdf2:iterations:salt_hex:key_hex" if (stored_hash.substr(0, 7) != "pbkdf2:") return false; size_t pos1 = 7; size_t pos2 = stored_hash.find(':', pos1); if (pos2 == std::string::npos) return false; size_t pos3 = stored_hash.find(':', pos2 + 1); if (pos3 == std::string::npos) return false; int iterations = std::stoi(stored_hash.substr(pos1, pos2 - pos1)); auto salt = hex_to_bytes(stored_hash.substr(pos2 + 1, pos3 - pos2 - 1)); auto expected_key = hex_to_bytes(stored_hash.substr(pos3 + 1)); unsigned char key[PBKDF2_KEY_LEN]; if (PKCS5_PBKDF2_HMAC( password.c_str(), static_cast(password.size()), salt.data(), static_cast(salt.size()), iterations, EVP_sha256(), PBKDF2_KEY_LEN, key) != 1) { return false; } // Constant-time comparison return CRYPTO_memcmp(key, expected_key.data(), std::min(static_cast(PBKDF2_KEY_LEN), expected_key.size())) == 0; } // --- DB-backed API --- void userdel(UserStore& db, const std::string& user) { db.delete_user(user); } bool verify_password(UserStore& db, const std::string& user, const std::string& pw) { auto stored = db.get_user_password(user); if (!stored || stored->empty()) return false; return verify_password(pw, *stored); } std::string issue_token(UserStore& db, const std::string& user) { std::string token = generate_token(); std::string token_hash = hash_token(token); db.update_user_token(user, token_hash); return token; } AuthResult verify_token(UserStore& db, const std::string& token) { std::string token_hash = hash_token(token); auto user_info = db.get_user_by_token_hash(token_hash); if (!user_info) return {}; return {true, user_info->username}; } bool request_origin_allowed(std::string_view method, std::string_view origin, std::string_view host) { // Safe methods never mutate state — CORS preflight (OPTIONS) included. if (method == "GET" || method == "HEAD" || method == "OPTIONS") return true; // No Origin header: curl, the CLI, the Hermes plugin, MCP — none send // one, and none are browser cross-site contexts. if (origin.empty()) return true; // Strip the scheme ("http://" / "https://") to get the authority. std::string_view authority = origin; if (auto sep = origin.find("://"); sep != std::string_view::npos) authority = origin.substr(sep + 3); if (authority.empty()) return false; // Case-insensitive exact match against Host (scheme-less). Hosts are // case-insensitive per RFC; ports must match too. auto ieq = [](std::string_view a, std::string_view b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { unsigned char ca = static_cast(a[i]); unsigned char cb = static_cast(b[i]); if (std::tolower(ca) != std::tolower(cb)) return false; } return true; }; return ieq(authority, host); } } // namespace ragger