fieldwitness/frontends/web/app.py
Aaron D. Lee b8d4eb5933 Add core modules, web frontend, CLI, keystore, and fieldkit
Core:
- paths.py: centralized ~/.soosef/ path constants
- config.py: JSON config loader with dataclass defaults
- exceptions.py: SoosefError hierarchy
- cli.py: unified Click CLI wrapping stegasoo + verisoo + native commands

Keystore:
- manager.py: unified key management (Ed25519 identity + channel keys)
- models.py: IdentityInfo, KeystoreStatus dataclasses
- export.py: encrypted key bundle export/import for USB transfer

Fieldkit:
- killswitch.py: ordered emergency data destruction (keys first)
- deadman.py: dead man's switch with check-in timer
- tamper.py: SHA-256 file integrity baseline + checking
- usb_monitor.py: pyudev USB whitelist enforcement
- geofence.py: haversine-based GPS boundary checking

Web frontend (Flask app factory + blueprints):
- app.py: create_app() factory with context processor
- blueprints: stego, attest, fieldkit, keys, admin
- templates: base.html (dark theme, unified nav), dashboard, all section pages
- static: CSS, favicon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:30:13 -04:00

172 lines
5.6 KiB
Python

"""
SooSeF Web Frontend
Flask application factory that unifies Stegasoo (steganography) and Verisoo
(provenance attestation) into a single web UI with fieldkit security features.
Built on Stegasoo's production-grade web UI patterns:
- Subprocess isolation for crash-safe stegasoo operations
- Async jobs with progress polling for large images
- Context processors for global template variables
- File-based temp storage with auto-expiry
BLUEPRINT STRUCTURE
===================
/ → index (dashboard)
/login, /logout → auth (adapted from stegasoo)
/setup → first-run wizard
/encode, /decode, /generate, /tools → stego blueprint
/attest, /verify → attest blueprint
/fieldkit/* → fieldkit blueprint
/keys/* → keys blueprint
/admin/* → admin blueprint
"""
import os
import secrets
import sys
from pathlib import Path
from flask import Flask, redirect, render_template, url_for
import soosef
from soosef.config import SoosefConfig
from soosef.paths import AUTH_DB, INSTANCE_DIR, SECRET_KEY_FILE, TEMP_DIR, ensure_dirs
# Suppress numpy/scipy warnings in subprocesses
os.environ["NUMPY_MADVISE_HUGEPAGE"] = "0"
os.environ["OMP_NUM_THREADS"] = "1"
# Maximum upload size (50 MB default)
MAX_FILE_SIZE = 50 * 1024 * 1024
def create_app(config: SoosefConfig | None = None) -> Flask:
"""Application factory."""
config = config or SoosefConfig.load()
ensure_dirs()
app = Flask(
__name__,
instance_path=str(INSTANCE_DIR),
template_folder=str(Path(__file__).parent / "templates"),
static_folder=str(Path(__file__).parent / "static"),
)
app.config["MAX_CONTENT_LENGTH"] = config.max_upload_mb * 1024 * 1024
app.config["AUTH_ENABLED"] = config.auth_enabled
app.config["HTTPS_ENABLED"] = config.https_enabled
app.config["SOOSEF_CONFIG"] = config
# Persist secret key so sessions survive restarts
_load_secret_key(app)
# ── Register blueprints ───────────────────────────────────────
from frontends.web.blueprints.stego import bp as stego_bp
from frontends.web.blueprints.attest import bp as attest_bp
from frontends.web.blueprints.fieldkit import bp as fieldkit_bp
from frontends.web.blueprints.keys import bp as keys_bp
from frontends.web.blueprints.admin import bp as admin_bp
app.register_blueprint(stego_bp)
app.register_blueprint(attest_bp)
app.register_blueprint(fieldkit_bp)
app.register_blueprint(keys_bp)
app.register_blueprint(admin_bp)
# ── Context processor (injected into ALL templates) ───────────
@app.context_processor
def inject_globals():
from soosef.keystore import KeystoreManager
ks = KeystoreManager()
ks_status = ks.status()
# Check fieldkit alert level
fieldkit_status = "ok"
if config.deadman_enabled:
from soosef.fieldkit.deadman import DeadmanSwitch
dm = DeadmanSwitch()
if dm.should_fire():
fieldkit_status = "alarm"
elif dm.is_overdue():
fieldkit_status = "warn"
# Check stegasoo capabilities
try:
from stegasoo import has_dct_support, HAS_AUDIO_SUPPORT
has_dct = has_dct_support()
has_audio = HAS_AUDIO_SUPPORT
except ImportError:
has_dct = False
has_audio = False
# Check verisoo availability
try:
import verisoo # noqa: F401
has_verisoo = True
except ImportError:
has_verisoo = False
return {
"version": soosef.__version__,
"has_dct": has_dct,
"has_audio": has_audio,
"has_verisoo": has_verisoo,
"has_fieldkit": config.killswitch_enabled or config.deadman_enabled,
"fieldkit_status": fieldkit_status,
"channel_configured": ks_status.has_channel_key,
"channel_fingerprint": ks_status.channel_fingerprint or "",
"identity_configured": ks_status.has_identity,
"identity_fingerprint": ks_status.identity_fingerprint or "",
"auth_enabled": app.config["AUTH_ENABLED"],
"is_authenticated": _is_authenticated(),
"is_admin": _is_admin(),
"username": _get_username(),
}
# ── Root routes ───────────────────────────────────────────────
@app.route("/")
def index():
return render_template("index.html")
return app
def _load_secret_key(app: Flask) -> None:
"""Load or generate persistent secret key for Flask sessions."""
SECRET_KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
if SECRET_KEY_FILE.exists():
app.secret_key = SECRET_KEY_FILE.read_bytes()
else:
key = secrets.token_bytes(32)
SECRET_KEY_FILE.write_bytes(key)
SECRET_KEY_FILE.chmod(0o600)
app.secret_key = key
def _is_authenticated() -> bool:
"""Check if current request has an authenticated session."""
# TODO: Wire up auth.py from stegasoo
return True
def _is_admin() -> bool:
"""Check if current user is an admin."""
# TODO: Wire up auth.py from stegasoo
return True
def _get_username() -> str:
"""Get current user's username."""
# TODO: Wire up auth.py from stegasoo
return "admin"