Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ Support the developer of this open-source project
```

**1. Clone and run:**
The new version of INTERCEPT includes PEPPER as a security enhancement, so all users, both new and existing, must follow the steps below to use the platform. SAVE your PEPPER for future updates or you will not be able to log in with your usual user account.

Generate a secure PEPPER with "openssl rand -hex 32" and save it in your computer. This is necessary if you are deploying with requirements or UV. In case of ./setup.sh the PEPPER will be ask you to generate during the installation.

macOS:

echo 'export INTERCEPT_PEPPER="your_generated_secret_here"' >> ~/.zshrc
source ~/.zshrc

Linux:
echo 'export INTERCEPT_PEPPER="your_generated_secret_here"' >> ~/.bashrc
source ~/.bashrc

```bash
git clone https://github.com/smittix/intercept.git
cd intercept
Expand All @@ -53,7 +66,8 @@ sudo -E venv/bin/python intercept.py
```

### Docker (Alternative)

Generate a secure PEPPER with "openssl rand -hex 32".
Modify on the docker-compose the INTERCEPT_PEPPER=your_generated_secret_here
```bash
git clone https://github.com/smittix/intercept.git
cd intercept
Expand Down
31 changes: 11 additions & 20 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys
import site

from utils.database import get_db
from utils.database import verify_user

# Ensure user site-packages is available (may be disabled when running as root/sudo)
if not site.ENABLE_USER_SITE:
Expand All @@ -27,7 +27,7 @@

from flask import Flask, render_template, jsonify, send_file, Response, request,redirect, url_for, flash, session
from werkzeug.security import check_password_hash
from config import VERSION, CHANGELOG
from config import PEPPER, VERSION, CHANGELOG
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
from utils.process import cleanup_stale_processes
from utils.sdr import SDRFactory
Expand Down Expand Up @@ -203,9 +203,9 @@ def add_security_headers(response):
# ============================================

@app.before_request
def require_login():
# Routes that don't require login (to avoid infinite redirect loop)
allowed_routes = ['login', 'static', 'favicon', 'health', 'health_check']
def require_login():
# Routes that don't require login (to avoid infinite redirect loop)
allowed_routes = ['login', 'static', 'favicon', 'health', 'health_check']

# If user is not logged in and the current route is not allowed...
if 'logged_in' not in session and request.endpoint not in allowed_routes:
Expand All @@ -217,26 +217,17 @@ def logout():
return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
@limiter.limit("5 per minute") # Limit to 5 login attempts per minute per IP
@limiter.limit("5 per minute")
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')

# Connect to DB and find user
with get_db() as conn:
cursor = conn.execute(
'SELECT password_hash, role FROM users WHERE username = ?',
(username,)
)
user = cursor.fetchone()
password = request.form.get('password') or ''
user_data = verify_user(username, password)

# Verify user exists and password is correct
if user and check_password_hash(user['password_hash'], password):
# Store data in session
if user_data:
session['logged_in'] = True
session['username'] = username
session['role'] = user['role']
session['role'] = user_data['role']

logger.info(f"User '{username}' logged in successfully.")
return redirect(url_for('index'))
Expand Down Expand Up @@ -710,4 +701,4 @@ def main() -> None:
debug=args.debug,
threaded=True,
load_dotenv=False,
)
)
25 changes: 13 additions & 12 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,18 @@ def _get_env_bool(key: str, default: bool) -> bool:
BT_SCAN_TIMEOUT = _get_env_int('BT_SCAN_TIMEOUT', 10)
BT_UPDATE_INTERVAL = _get_env_float('BT_UPDATE_INTERVAL', 2.0)

# ADS-B settings
ADSB_SBS_PORT = _get_env_int('ADSB_SBS_PORT', 30003)
ADSB_UPDATE_INTERVAL = _get_env_float('ADSB_UPDATE_INTERVAL', 1.0)
ADSB_HISTORY_ENABLED = _get_env_bool('ADSB_HISTORY_ENABLED', False)
ADSB_DB_HOST = _get_env('ADSB_DB_HOST', 'localhost')
ADSB_DB_PORT = _get_env_int('ADSB_DB_PORT', 5432)
ADSB_DB_NAME = _get_env('ADSB_DB_NAME', 'intercept_adsb')
ADSB_DB_USER = _get_env('ADSB_DB_USER', 'intercept')
ADSB_DB_PASSWORD = _get_env('ADSB_DB_PASSWORD', 'intercept')
ADSB_HISTORY_BATCH_SIZE = _get_env_int('ADSB_HISTORY_BATCH_SIZE', 500)
ADSB_HISTORY_FLUSH_INTERVAL = _get_env_float('ADSB_HISTORY_FLUSH_INTERVAL', 1.0)
ADSB_HISTORY_QUEUE_SIZE = _get_env_int('ADSB_HISTORY_QUEUE_SIZE', 50000)
# ADS-B settings
ADSB_SBS_PORT = _get_env_int('ADSB_SBS_PORT', 30003)
ADSB_UPDATE_INTERVAL = _get_env_float('ADSB_UPDATE_INTERVAL', 1.0)
ADSB_HISTORY_ENABLED = _get_env_bool('ADSB_HISTORY_ENABLED', False)
ADSB_DB_HOST = _get_env('ADSB_DB_HOST', 'localhost')
ADSB_DB_PORT = _get_env_int('ADSB_DB_PORT', 5432)
ADSB_DB_NAME = _get_env('ADSB_DB_NAME', 'intercept_adsb')
ADSB_DB_USER = _get_env('ADSB_DB_USER', 'intercept')
ADSB_DB_PASSWORD = _get_env('ADSB_DB_PASSWORD', 'intercept')
ADSB_HISTORY_BATCH_SIZE = _get_env_int('ADSB_HISTORY_BATCH_SIZE', 500)
ADSB_HISTORY_FLUSH_INTERVAL = _get_env_float('ADSB_HISTORY_FLUSH_INTERVAL', 1.0)
ADSB_HISTORY_QUEUE_SIZE = _get_env_int('ADSB_HISTORY_QUEUE_SIZE', 50000)

# Satellite settings
SATELLITE_UPDATE_INTERVAL = _get_env_int('SATELLITE_UPDATE_INTERVAL', 30)
Expand All @@ -147,6 +147,7 @@ def _get_env_bool(key: str, default: bool) -> bool:
# Admin credentials
ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin')
ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', 'admin')
PEPPER = os.environ.get('INTERCEPT_PEPPER')

def configure_logging() -> None:
"""Configure application logging."""
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ services:
- INTERCEPT_HOST=0.0.0.0
- INTERCEPT_PORT=5050
- INTERCEPT_LOG_LEVEL=INFO
- INTERCEPT_PEPPER=your_generated_secret_here
# ADS-B history is disabled by default
# To enable, use: docker compose --profile history up -d
# - INTERCEPT_ADSB_HISTORY_ENABLED=true
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ dependencies = [
"bleak>=0.21.0",
"flask-sock",
"requests>=2.28.0",
'psycopg2-binary>=2.9.9',
'numpy>=1.24.0',
'scipy>=1.10.0'
]

[project.urls]
Expand Down
33 changes: 33 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -877,11 +877,44 @@ install_debian_packages() {
fi
}

# ----------------------------
# Security / Pepper Check
# ----------------------------
check_pepper() {
echo
info "Checking Security Configuration..."

if [[ -z "${INTERCEPT_PEPPER:-}" ]]; then
warn "INTERCEPT_PEPPER is not set in your environment."
echo -e "For security, you must generate a unique Pepper for password hashing."

if ask_yes_no "Would you like me to generate one and show you how to save it?"; then
local NEW_PEPPER
NEW_PEPPER=$(openssl rand -hex 32)
ok "Generated Pepper: ${NEW_PEPPER}"
echo
echo "To make this permanent, run the following command:"
if [[ "$OS" == "macos" ]]; then
echo -e "${YELLOW}echo 'export INTERCEPT_PEPPER=\"$NEW_PEPPER\"' >> ~/.zshrc && source ~/.zshrc${NC}"
else
echo -e "${YELLOW}echo 'export INTERCEPT_PEPPER=\"$NEW_PEPPER\"' >> ~/.bashrc && source ~/.bashrc${NC}"
fi
echo
warn "IMPORTANT: Save this secret! If you lose it, you will be locked out of your database."
else
fail "Warning: INTERCEPT will fail to start without INTERCEPT_PEPPER."
fi
else
ok "INTERCEPT_PEPPER is already set."
fi
}

# ----------------------------
# Final summary / hard fail
# ----------------------------
final_summary_and_hard_fail() {
check_tools
check_pepper

echo "============================================"
echo
Expand Down
60 changes: 52 additions & 8 deletions utils/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from datetime import datetime
from pathlib import Path
from typing import Any
from werkzeug.security import generate_password_hash
from config import ADMIN_USERNAME, ADMIN_PASSWORD
from werkzeug.security import generate_password_hash, check_password_hash
from config import ADMIN_USERNAME, ADMIN_PASSWORD, PEPPER

logger = logging.getLogger('intercept.database')

Expand All @@ -24,6 +24,55 @@
# Thread-local storage for connections
_local = threading.local()

from werkzeug.security import check_password_hash, generate_password_hash

def verify_user(username: str, password: str) -> dict | None:
"""
Verifies user credentials. If a legacy hash is found, it migrates
the user to the new PEPPER-based hashing automatically.
Returns user data (role) if successful, None otherwise.
"""
with get_db() as conn:
print(f"Verifying user: {username} at {datetime.now()}")
cursor = conn.execute(
'SELECT id, password_hash, role FROM users WHERE username = ?',
(username,)
)
user = cursor.fetchone()

if not user:
return None

user_id = user['id']
current_hash = user['password_hash']
user_role = user['role']

# 1. New verification (Password + Pepper)
if check_password_hash(current_hash, f"{password}{PEPPER}"):
print(f"User '{username}' new verified at {datetime.now()}")
return {"role": user_role}

# 2. Legacy fallback (Password only)
if check_password_hash(current_hash, password):
print(f"User '{username}' legacy verified at {datetime.now()}")
logger.info(f"Legacy hash detected for user '{username}'. Migrating...")

# Upgrade the hash to include the Pepper
new_hash = generate_password_hash(f"{password}{PEPPER}")

try:
with get_db() as conn:
conn.execute(
'UPDATE users SET password_hash = ? WHERE id = ?',
(new_hash, user_id)
)
return {"role": user_role}
except Exception as e:
logger.error(f"Migration failed for {username}, but granting access: {e}")
return {"role": user_role}

return None


def get_db_path() -> Path:
"""Get the database file path, creating directory if needed."""
Expand Down Expand Up @@ -115,12 +164,7 @@ def init_db() -> None:

cursor = conn.execute('SELECT COUNT(*) FROM users')
if cursor.fetchone()[0] == 0:
from config import ADMIN_USERNAME, ADMIN_PASSWORD

logger.info(f"Creating default admin user: {ADMIN_USERNAME}")

# Password hashing
hashed_pw = generate_password_hash(ADMIN_PASSWORD)
hashed_pw = generate_password_hash(f"{ADMIN_PASSWORD}{PEPPER}")

conn.execute('''
INSERT INTO users (username, password_hash, role)
Expand Down