Fix all 98 ruff lint errors across codebase
Some checks failed
CI / lint (push) Successful in 46s
CI / typecheck (push) Failing after 22s
CI / test (push) Failing after 20s

- Remove unused imports (app.py, stego_routes.py, killswitch.py, etc.)
- Sort import blocks (I001)
- Add missing os import in stego_routes.py (F821)
- Rename shadowed Click commands to avoid F811 (status→chain_status, show→chain_show)
- Rename uppercase locals R→earth_r, _HAS_QRCODE_READ→_has_qrcode_read (N806)
- Suppress false-positive F821 for get_username (closure scope)
- Use datetime.UTC alias (UP017)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee 2026-04-01 18:30:01 -04:00
parent 5c74a5f4aa
commit 17147856d1
15 changed files with 127 additions and 174 deletions

View File

@ -25,13 +25,9 @@ SooSeF-native features (attest, fieldkit, keys) are clean blueprints.
""" """
import io import io
import mimetypes
import os import os
import secrets import secrets
import sys import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
from flask import ( from flask import (
@ -45,11 +41,10 @@ from flask import (
session, session,
url_for, url_for,
) )
from PIL import Image
import soosef import soosef
from soosef.config import SoosefConfig from soosef.config import SoosefConfig
from soosef.paths import AUTH_DB, INSTANCE_DIR, SECRET_KEY_FILE, TEMP_DIR, ensure_dirs from soosef.paths import INSTANCE_DIR, SECRET_KEY_FILE, TEMP_DIR, ensure_dirs
# Suppress numpy/scipy warnings in subprocesses # Suppress numpy/scipy warnings in subprocesses
os.environ["NUMPY_MADVISE_HUGEPAGE"] = "0" os.environ["NUMPY_MADVISE_HUGEPAGE"] = "0"
@ -89,7 +84,8 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
# Add web dir to path so auth.py and support modules are importable # Add web dir to path so auth.py and support modules are importable
sys.path.insert(0, str(web_dir)) sys.path.insert(0, str(web_dir))
from auth import init_app as init_auth, is_authenticated, is_admin, get_username from auth import get_username, is_admin, is_authenticated
from auth import init_app as init_auth
init_auth(app) init_auth(app)
@ -127,19 +123,17 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
# Stegasoo capabilities # Stegasoo capabilities
try: try:
from stegasoo import has_dct_support, HAS_AUDIO_SUPPORT from stegasoo import HAS_AUDIO_SUPPORT, get_channel_status, has_dct_support
from stegasoo import get_channel_status
from stegasoo.constants import ( from stegasoo.constants import (
MAX_MESSAGE_CHARS,
MAX_FILE_PAYLOAD_SIZE,
MAX_UPLOAD_SIZE,
TEMP_FILE_EXPIRY_MINUTES,
MIN_PIN_LENGTH,
MAX_PIN_LENGTH,
MIN_PASSPHRASE_WORDS,
RECOMMENDED_PASSPHRASE_WORDS,
DEFAULT_PASSPHRASE_WORDS, DEFAULT_PASSPHRASE_WORDS,
__version__ as stegasoo_version, MAX_FILE_PAYLOAD_SIZE,
MAX_MESSAGE_CHARS,
MAX_PIN_LENGTH,
MAX_UPLOAD_SIZE,
MIN_PASSPHRASE_WORDS,
MIN_PIN_LENGTH,
RECOMMENDED_PASSPHRASE_WORDS,
TEMP_FILE_EXPIRY_MINUTES,
) )
has_dct = has_dct_support() has_dct = has_dct_support()
@ -246,68 +240,30 @@ def _register_stegasoo_routes(app: Flask) -> None:
The stegasoo templates are in templates/stego/ and extend our base.html. The stegasoo templates are in templates/stego/ and extend our base.html.
""" """
import temp_storage import temp_storage
from soosef.audit import log_action from auth import admin_required, login_required
from subprocess_stego import (
SubprocessStego,
cleanup_progress_file,
generate_job_id,
get_progress_file_path,
read_progress,
)
from auth import login_required, admin_required
import stegasoo
from stegasoo import ( from stegasoo import (
HAS_AUDIO_SUPPORT,
CapacityError,
DecryptionError,
FilePayload,
InvalidHeaderError,
InvalidMagicBytesError,
ReedSolomonError,
StegasooError,
export_rsa_key_pem, export_rsa_key_pem,
generate_credentials, generate_credentials,
generate_filename,
get_channel_status, get_channel_status,
has_argon2,
has_dct_support,
load_rsa_key, load_rsa_key,
validate_channel_key,
validate_file_payload,
validate_image,
validate_message,
validate_passphrase,
validate_pin,
validate_rsa_key,
validate_security_factors,
) )
from stegasoo.constants import ( from stegasoo.constants import (
DEFAULT_PASSPHRASE_WORDS, DEFAULT_PASSPHRASE_WORDS,
MAX_FILE_PAYLOAD_SIZE,
MAX_FILE_SIZE,
MAX_MESSAGE_CHARS,
MAX_PIN_LENGTH, MAX_PIN_LENGTH,
MAX_UPLOAD_SIZE,
MIN_PASSPHRASE_WORDS, MIN_PASSPHRASE_WORDS,
MIN_PIN_LENGTH, MIN_PIN_LENGTH,
RECOMMENDED_PASSPHRASE_WORDS,
TEMP_FILE_EXPIRY, TEMP_FILE_EXPIRY,
TEMP_FILE_EXPIRY_MINUTES,
THUMBNAIL_QUALITY,
THUMBNAIL_SIZE,
VALID_RSA_SIZES, VALID_RSA_SIZES,
__version__,
) )
from stegasoo.qr_utils import ( from stegasoo.qr_utils import (
can_fit_in_qr, can_fit_in_qr,
decompress_data,
detect_and_crop_qr,
extract_key_from_qr,
generate_qr_code, generate_qr_code,
is_compressed,
) )
from stegasoo.channel import resolve_channel_key from subprocess_stego import (
SubprocessStego,
)
from soosef.audit import log_action
# Initialize subprocess wrapper # Initialize subprocess wrapper
subprocess_stego = SubprocessStego(timeout=180) subprocess_stego = SubprocessStego(timeout=180)
@ -315,36 +271,36 @@ def _register_stegasoo_routes(app: Flask) -> None:
# ── Auth routes (setup, login, logout, account) ──────────────── # ── Auth routes (setup, login, logout, account) ────────────────
from auth import ( from auth import (
create_admin_user, MAX_CHANNEL_KEYS,
verify_user_password, MAX_USERS,
login_user as auth_login_user, can_create_user,
logout_user as auth_logout_user, can_save_channel_key,
is_authenticated as auth_is_authenticated,
user_exists as auth_user_exists,
get_current_user,
get_recovery_key_hash,
has_recovery_key,
set_recovery_key_hash,
verify_and_reset_admin_password,
change_password, change_password,
get_all_users, create_admin_user,
create_user, create_user,
delete_user, delete_user,
get_user_by_id,
reset_user_password,
generate_temp_password, generate_temp_password,
can_create_user, get_all_users,
get_non_admin_count, get_current_user,
get_recovery_key_hash,
get_user_by_id,
get_user_channel_keys, get_user_channel_keys,
save_channel_key, has_recovery_key,
delete_channel_key, reset_user_password,
can_save_channel_key, verify_and_reset_admin_password,
update_channel_key_name, verify_user_password,
update_channel_key_last_used, )
get_channel_key_by_id, from auth import (
clear_recovery_key, is_authenticated as auth_is_authenticated,
MAX_USERS, )
MAX_CHANNEL_KEYS, from auth import (
login_user as auth_login_user,
)
from auth import (
logout_user as auth_logout_user,
)
from auth import (
user_exists as auth_user_exists,
) )
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
@ -469,7 +425,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
temp_password = generate_temp_password() temp_password = generate_temp_password()
success, message = create_user(username, temp_password) success, message = create_user(username, temp_password)
log_action( log_action(
actor=get_username(), actor=get_username(), # noqa: F821
action="user.create", action="user.create",
target=f"user:{username}", target=f"user:{username}",
outcome="success" if success else "failure", outcome="success" if success else "failure",
@ -492,7 +448,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
target_name = target_user.username if target_user else str(user_id) target_name = target_user.username if target_user else str(user_id)
success, message = delete_user(user_id, get_current_user().id) success, message = delete_user(user_id, get_current_user().id)
log_action( log_action(
actor=get_username(), actor=get_username(), # noqa: F821
action="user.delete", action="user.delete",
target=f"user:{target_name}", target=f"user:{target_name}",
outcome="success" if success else "failure", outcome="success" if success else "failure",
@ -510,7 +466,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
target_user = get_user_by_id(user_id) target_user = get_user_by_id(user_id)
target_name = target_user.username if target_user else str(user_id) target_name = target_user.username if target_user else str(user_id)
log_action( log_action(
actor=get_username(), actor=get_username(), # noqa: F821
action="user.password_reset", action="user.password_reset",
target=f"user:{target_name}", target=f"user:{target_name}",
outcome="success" if success else "failure", outcome="success" if success else "failure",

View File

@ -13,9 +13,8 @@ import json
import socket import socket
from datetime import UTC, datetime from datetime import UTC, datetime
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
from auth import login_required from auth import login_required
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
bp = Blueprint("attest", __name__) bp = Blueprint("attest", __name__)
@ -23,6 +22,7 @@ bp = Blueprint("attest", __name__)
def _get_storage(): def _get_storage():
"""Get verisoo LocalStorage pointed at soosef's attestation directory.""" """Get verisoo LocalStorage pointed at soosef's attestation directory."""
from verisoo.storage import LocalStorage from verisoo.storage import LocalStorage
from soosef.paths import ATTESTATIONS_DIR from soosef.paths import ATTESTATIONS_DIR
return LocalStorage(base_path=ATTESTATIONS_DIR) return LocalStorage(base_path=ATTESTATIONS_DIR)
@ -31,6 +31,7 @@ def _get_storage():
def _get_private_key(): def _get_private_key():
"""Load the Ed25519 private key from soosef identity directory.""" """Load the Ed25519 private key from soosef identity directory."""
from verisoo.crypto import load_private_key from verisoo.crypto import load_private_key
from soosef.paths import IDENTITY_PRIVATE_KEY from soosef.paths import IDENTITY_PRIVATE_KEY
if not IDENTITY_PRIVATE_KEY.exists(): if not IDENTITY_PRIVATE_KEY.exists():
@ -165,8 +166,8 @@ def attest():
) )
# Save our own identity so we can look it up during verification # Save our own identity so we can look it up during verification
from verisoo.models import Identity
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from verisoo.models import Identity
pub_key = private_key.public_key() pub_key = private_key.public_key()
pub_bytes = pub_key.public_bytes(Encoding.Raw, PublicFormat.Raw) pub_bytes = pub_key.public_bytes(Encoding.Raw, PublicFormat.Raw)

View File

@ -2,9 +2,9 @@
Fieldkit blueprint killswitch, dead man's switch, status dashboard. Fieldkit blueprint killswitch, dead man's switch, status dashboard.
""" """
from auth import admin_required, get_username, login_required
from flask import Blueprint, flash, redirect, render_template, request, url_for from flask import Blueprint, flash, redirect, render_template, request, url_for
from auth import admin_required, get_username, login_required
from soosef.audit import log_action from soosef.audit import log_action
bp = Blueprint("fieldkit", __name__, url_prefix="/fieldkit") bp = Blueprint("fieldkit", __name__, url_prefix="/fieldkit")

View File

@ -2,9 +2,9 @@
Key management blueprint unified view of all key material. Key management blueprint unified view of all key material.
""" """
from flask import Blueprint, flash, redirect, render_template, request, url_for
from auth import get_username, login_required from auth import get_username, login_required
from flask import Blueprint, flash, redirect, render_template, url_for
from soosef.audit import log_action from soosef.audit import log_action
bp = Blueprint("keys", __name__, url_prefix="/keys") bp = Blueprint("keys", __name__, url_prefix="/keys")

View File

@ -14,6 +14,7 @@ All routes use subprocess isolation via SubprocessStego for crash safety.
import io import io
import mimetypes import mimetypes
import os
import secrets import secrets
import threading import threading
import time import time
@ -38,7 +39,7 @@ def register_stego_routes(app, **deps):
login_required = deps["login_required"] login_required = deps["login_required"]
subprocess_stego = deps["subprocess_stego"] subprocess_stego = deps["subprocess_stego"]
temp_storage = deps["temp_storage"] temp_storage = deps["temp_storage"]
_HAS_QRCODE_READ = deps.get("has_qrcode_read", False) _has_qrcode_read = deps.get("has_qrcode_read", False)
from stegasoo import ( from stegasoo import (
HAS_AUDIO_SUPPORT, HAS_AUDIO_SUPPORT,
@ -59,14 +60,12 @@ def register_stego_routes(app, **deps):
validate_rsa_key, validate_rsa_key,
validate_security_factors, validate_security_factors,
) )
from stegasoo.channel import resolve_channel_key
from stegasoo.constants import ( from stegasoo.constants import (
MAX_FILE_PAYLOAD_SIZE,
MAX_MESSAGE_CHARS,
TEMP_FILE_EXPIRY, TEMP_FILE_EXPIRY,
THUMBNAIL_QUALITY, THUMBNAIL_QUALITY,
THUMBNAIL_SIZE, THUMBNAIL_SIZE,
) )
from stegasoo.channel import resolve_channel_key
from stegasoo.qr_utils import ( from stegasoo.qr_utils import (
decompress_data, decompress_data,
extract_key_from_qr, extract_key_from_qr,
@ -361,7 +360,7 @@ def register_stego_routes(app, **deps):
if is_async: if is_async:
return jsonify({"error": msg}), 400 return jsonify({"error": msg}), 400
flash(msg, "error") flash(msg, "error")
return render_template("stego/encode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/encode.html", has_qrcode_read=_has_qrcode_read)
try: try:
# Get files # Get files
@ -457,7 +456,7 @@ def register_stego_routes(app, **deps):
rsa_key_from_qr = True rsa_key_from_qr = True
elif rsa_key_file and rsa_key_file.filename: elif rsa_key_file and rsa_key_file.filename:
rsa_key_data = rsa_key_file.read() rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and _HAS_QRCODE_READ: elif rsa_key_qr and rsa_key_qr.filename and _has_qrcode_read:
qr_image_data = rsa_key_qr.read() qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data) key_pem = extract_key_from_qr(qr_image_data)
if key_pem: if key_pem:
@ -652,7 +651,7 @@ def register_stego_routes(app, **deps):
rsa_key_from_qr = True rsa_key_from_qr = True
elif rsa_key_file and rsa_key_file.filename: elif rsa_key_file and rsa_key_file.filename:
rsa_key_data = rsa_key_file.read() rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and _HAS_QRCODE_READ: elif rsa_key_qr and rsa_key_qr.filename and _has_qrcode_read:
qr_image_data = rsa_key_qr.read() qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data) key_pem = extract_key_from_qr(qr_image_data)
if key_pem: if key_pem:
@ -819,7 +818,7 @@ def register_stego_routes(app, **deps):
except Exception as e: except Exception as e:
return _error_response(f"Error: {e}") return _error_response(f"Error: {e}")
return render_template("stego/encode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/encode.html", has_qrcode_read=_has_qrcode_read)
# ============================================================================ # ============================================================================
# ENCODE PROGRESS ENDPOINTS (v4.1.2) # ENCODE PROGRESS ENDPOINTS (v4.1.2)
@ -1134,25 +1133,25 @@ def register_stego_routes(app, **deps):
if not HAS_AUDIO_SUPPORT: if not HAS_AUDIO_SUPPORT:
flash("Audio steganography is not available.", "error") flash("Audio steganography is not available.", "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
if not ref_photo or not stego_image: if not ref_photo or not stego_image:
flash("Both reference photo and stego audio are required", "error") flash("Both reference photo and stego audio are required", "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
if not allowed_image(ref_photo.filename): if not allowed_image(ref_photo.filename):
flash("Reference must be an image", "error") flash("Reference must be an image", "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
if not allowed_audio(stego_image.filename): if not allowed_audio(stego_image.filename):
flash("Invalid audio format", "error") flash("Invalid audio format", "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
passphrase = request.form.get("passphrase", "") passphrase = request.form.get("passphrase", "")
@ -1168,7 +1167,7 @@ def register_stego_routes(app, **deps):
if not passphrase: if not passphrase:
flash("Passphrase is required", "error") flash("Passphrase is required", "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
ref_data = ref_photo.read() ref_data = ref_photo.read()
@ -1187,7 +1186,7 @@ def register_stego_routes(app, **deps):
rsa_key_from_qr = True rsa_key_from_qr = True
elif rsa_key_file and rsa_key_file.filename: elif rsa_key_file and rsa_key_file.filename:
rsa_key_data = rsa_key_file.read() rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and _HAS_QRCODE_READ: elif rsa_key_qr and rsa_key_qr.filename and _has_qrcode_read:
qr_image_data = rsa_key_qr.read() qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data) key_pem = extract_key_from_qr(qr_image_data)
if key_pem: if key_pem:
@ -1196,14 +1195,14 @@ def register_stego_routes(app, **deps):
else: else:
flash("Could not extract RSA key from QR code image.", "error") flash("Could not extract RSA key from QR code image.", "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
result = validate_security_factors(pin, rsa_key_data) result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, "error") flash(result.error_message, "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
if pin: if pin:
@ -1211,7 +1210,7 @@ def register_stego_routes(app, **deps):
if not result.is_valid: if not result.is_valid:
flash(result.error_message, "error") flash(result.error_message, "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
key_password = ( key_password = (
@ -1223,7 +1222,7 @@ def register_stego_routes(app, **deps):
if not result.is_valid: if not result.is_valid:
flash(result.error_message, "error") flash(result.error_message, "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
is_async = ( is_async = (
@ -1274,7 +1273,7 @@ def register_stego_routes(app, **deps):
else: else:
flash(error_msg, "error") flash(error_msg, "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
if decode_result.is_file: if decode_result.is_file:
@ -1296,19 +1295,19 @@ def register_stego_routes(app, **deps):
filename=filename, filename=filename,
file_size=format_size(len(decode_result.file_data)), file_size=format_size(len(decode_result.file_data)),
mime_type=decode_result.mime_type, mime_type=decode_result.mime_type,
has_qrcode_read=_HAS_QRCODE_READ, has_qrcode_read=_has_qrcode_read,
) )
else: else:
return render_template( return render_template(
"decode.html", "decode.html",
decoded_message=decode_result.message, decoded_message=decode_result.message,
has_qrcode_read=_HAS_QRCODE_READ, has_qrcode_read=_has_qrcode_read,
) )
# ========== IMAGE DECODE PATH (original) ========== # ========== IMAGE DECODE PATH (original) ==========
if not ref_photo or not stego_image: if not ref_photo or not stego_image:
flash("Both reference photo and stego image are required", "error") flash("Both reference photo and stego image are required", "error")
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
# Get form data - v3.2.0: renamed from day_phrase to passphrase # Get form data - v3.2.0: renamed from day_phrase to passphrase
passphrase = request.form.get("passphrase", "") # v3.2.0: Renamed passphrase = request.form.get("passphrase", "") # v3.2.0: Renamed
@ -1326,14 +1325,14 @@ def register_stego_routes(app, **deps):
# Check DCT availability # Check DCT availability
if embed_mode == "dct" and not has_dct_support(): if embed_mode == "dct" and not has_dct_support():
flash("DCT mode requires scipy. Install with: pip install scipy", "error") flash("DCT mode requires scipy. Install with: pip install scipy", "error")
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
# v3.2.0: Removed date handling (no stego_date needed) # v3.2.0: Removed date handling (no stego_date needed)
# v3.2.0: Renamed from day_phrase # v3.2.0: Renamed from day_phrase
if not passphrase: if not passphrase:
flash("Passphrase is required", "error") flash("Passphrase is required", "error")
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
# Read files # Read files
ref_data = ref_photo.read() ref_data = ref_photo.read()
@ -1353,7 +1352,7 @@ def register_stego_routes(app, **deps):
rsa_key_from_qr = True rsa_key_from_qr = True
elif rsa_key_file and rsa_key_file.filename: elif rsa_key_file and rsa_key_file.filename:
rsa_key_data = rsa_key_file.read() rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and _HAS_QRCODE_READ: elif rsa_key_qr and rsa_key_qr.filename and _has_qrcode_read:
qr_image_data = rsa_key_qr.read() qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data) key_pem = extract_key_from_qr(qr_image_data)
if key_pem: if key_pem:
@ -1362,14 +1361,14 @@ def register_stego_routes(app, **deps):
else: else:
flash("Could not extract RSA key from QR code image.", "error") flash("Could not extract RSA key from QR code image.", "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
# Validate security factors # Validate security factors
result = validate_security_factors(pin, rsa_key_data) result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid: if not result.is_valid:
flash(result.error_message, "error") flash(result.error_message, "error")
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
# Validate PIN if provided # Validate PIN if provided
if pin: if pin:
@ -1377,7 +1376,7 @@ def register_stego_routes(app, **deps):
if not result.is_valid: if not result.is_valid:
flash(result.error_message, "error") flash(result.error_message, "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
# Determine key password # Determine key password
@ -1389,7 +1388,7 @@ def register_stego_routes(app, **deps):
if not result.is_valid: if not result.is_valid:
flash(result.error_message, "error") flash(result.error_message, "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
# Check for async mode (v4.1.5) # Check for async mode (v4.1.5)
@ -1437,7 +1436,7 @@ def register_stego_routes(app, **deps):
if "channel key" in error_msg.lower(): if "channel key" in error_msg.lower():
flash(error_msg, "error") flash(error_msg, "error")
return render_template( return render_template(
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ "stego/decode.html", has_qrcode_read=_has_qrcode_read
) )
if ( if (
"decrypt" in error_msg.lower() "decrypt" in error_msg.lower()
@ -1468,14 +1467,14 @@ def register_stego_routes(app, **deps):
filename=filename, filename=filename,
file_size=format_size(len(decode_result.file_data)), file_size=format_size(len(decode_result.file_data)),
mime_type=decode_result.mime_type, mime_type=decode_result.mime_type,
has_qrcode_read=_HAS_QRCODE_READ, has_qrcode_read=_has_qrcode_read,
) )
else: else:
# Text content # Text content
return render_template( return render_template(
"decode.html", "decode.html",
decoded_message=decode_result.message, decoded_message=decode_result.message,
has_qrcode_read=_HAS_QRCODE_READ, has_qrcode_read=_has_qrcode_read,
) )
except InvalidMagicBytesError: except InvalidMagicBytesError:
@ -1483,33 +1482,33 @@ def register_stego_routes(app, **deps):
"This doesn't appear to be a Stegasoo image. Try a different mode (LSB/DCT).", "This doesn't appear to be a Stegasoo image. Try a different mode (LSB/DCT).",
"warning", "warning",
) )
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
except ReedSolomonError: except ReedSolomonError:
flash( flash(
"Image too corrupted to decode. It may have been re-saved or compressed.", "Image too corrupted to decode. It may have been re-saved or compressed.",
"error", "error",
) )
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
except InvalidHeaderError: except InvalidHeaderError:
flash( flash(
"Invalid or corrupted header. The image may have been modified.", "Invalid or corrupted header. The image may have been modified.",
"error", "error",
) )
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
except DecryptionError: except DecryptionError:
flash( flash(
"Wrong credentials. Double-check your reference photo, passphrase, PIN, and channel key.", "Wrong credentials. Double-check your reference photo, passphrase, PIN, and channel key.",
"warning", "warning",
) )
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
except StegasooError as e: except StegasooError as e:
flash(str(e), "error") flash(str(e), "error")
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
except Exception as e: except Exception as e:
flash(f"Error: {e}", "error") flash(f"Error: {e}", "error")
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ) return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
@app.route("/decode/download/<file_id>") @app.route("/decode/download/<file_id>")
@login_required @login_required
@ -1602,20 +1601,20 @@ def register_stego_routes(app, **deps):
filename=job.get("filename"), filename=job.get("filename"),
file_size=format_size(job.get("file_size", 0)), file_size=format_size(job.get("file_size", 0)),
mime_type=job.get("mime_type"), mime_type=job.get("mime_type"),
has_qrcode_read=_HAS_QRCODE_READ, has_qrcode_read=_has_qrcode_read,
) )
else: else:
return render_template( return render_template(
"decode.html", "decode.html",
decoded_message=job.get("message"), decoded_message=job.get("message"),
has_qrcode_read=_HAS_QRCODE_READ, has_qrcode_read=_has_qrcode_read,
) )
@app.route("/about") @app.route("/about")
def about(): def about():
from stegasoo.channel import get_channel_status
from stegasoo import has_argon2
from auth import get_current_user from auth import get_current_user
from stegasoo import has_argon2
from stegasoo.channel import get_channel_status
channel_status = get_channel_status() channel_status = get_channel_status()
current_user = get_current_user() current_user = get_current_user()
@ -1624,7 +1623,7 @@ def register_stego_routes(app, **deps):
return render_template( return render_template(
"stego/about.html", "stego/about.html",
has_argon2=has_argon2(), has_argon2=has_argon2(),
has_qrcode_read=_HAS_QRCODE_READ, has_qrcode_read=_has_qrcode_read,
channel_configured=channel_status["configured"], channel_configured=channel_status["configured"],
channel_fingerprint=channel_status.get("fingerprint"), channel_fingerprint=channel_status.get("fingerprint"),
channel_source=channel_status.get("source"), channel_source=channel_status.get("source"),

View File

@ -36,7 +36,7 @@ from __future__ import annotations
import json import json
import logging import logging
import threading import threading
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
@ -82,7 +82,7 @@ def log_action(
detail: Optional free-text annotation (avoid PII where possible). detail: Optional free-text annotation (avoid PII where possible).
""" """
entry: dict[str, str] = { entry: dict[str, str] = {
"timestamp": datetime.now(tz=timezone.utc).isoformat(), "timestamp": datetime.now(tz=UTC).isoformat(),
"actor": actor, "actor": actor,
"action": action, "action": action,
"target": target, "target": target,

View File

@ -48,9 +48,9 @@ def main(ctx, data_dir, json_output):
@click.pass_context @click.pass_context
def init(ctx, no_identity, no_channel): def init(ctx, no_identity, no_channel):
"""Initialize a new SooSeF instance — generate keys and create directory structure.""" """Initialize a new SooSeF instance — generate keys and create directory structure."""
from soosef.paths import ensure_dirs
from soosef.keystore.manager import KeystoreManager
from soosef.config import SoosefConfig from soosef.config import SoosefConfig
from soosef.keystore.manager import KeystoreManager
from soosef.paths import ensure_dirs
click.echo("Initializing SooSeF...") click.echo("Initializing SooSeF...")
ensure_dirs() ensure_dirs()
@ -103,7 +103,7 @@ def serve(host, port, no_https, debug):
ssl_context = None ssl_context = None
if config.https_enabled: if config.https_enabled:
from soosef.paths import SSL_CERT, SSL_KEY, CERTS_DIR from soosef.paths import CERTS_DIR, SSL_CERT, SSL_KEY
CERTS_DIR.mkdir(parents=True, exist_ok=True) CERTS_DIR.mkdir(parents=True, exist_ok=True)
if not SSL_CERT.exists(): if not SSL_CERT.exists():
@ -173,11 +173,12 @@ def _start_deadman_thread(interval_seconds: int = 60) -> threading.Thread | None
def _generate_self_signed_cert(cert_path: Path, key_path: Path) -> None: def _generate_self_signed_cert(cert_path: Path, key_path: Path) -> None:
"""Generate a self-signed certificate for development/local use.""" """Generate a self-signed certificate for development/local use."""
from datetime import UTC, datetime, timedelta
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
from datetime import datetime, timedelta, UTC
key = rsa.generate_private_key(public_exponent=65537, key_size=2048) key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = issuer = x509.Name( subject = issuer = x509.Name(
@ -738,7 +739,7 @@ def show():
def export_keys(output, password): def export_keys(output, password):
"""Export all keys to an encrypted bundle file.""" """Export all keys to an encrypted bundle file."""
from soosef.keystore.export import export_bundle from soosef.keystore.export import export_bundle
from soosef.paths import IDENTITY_DIR, CHANNEL_KEY_FILE from soosef.paths import CHANNEL_KEY_FILE, IDENTITY_DIR
export_bundle(IDENTITY_DIR, CHANNEL_KEY_FILE, output, password.encode()) export_bundle(IDENTITY_DIR, CHANNEL_KEY_FILE, output, password.encode())
click.echo(f"Key bundle exported to: {output}") click.echo(f"Key bundle exported to: {output}")
@ -750,7 +751,7 @@ def export_keys(output, password):
def import_keys(bundle, password): def import_keys(bundle, password):
"""Import keys from an encrypted bundle file.""" """Import keys from an encrypted bundle file."""
from soosef.keystore.export import import_bundle from soosef.keystore.export import import_bundle
from soosef.paths import IDENTITY_DIR, CHANNEL_KEY_FILE from soosef.paths import CHANNEL_KEY_FILE, IDENTITY_DIR
imported = import_bundle(bundle, IDENTITY_DIR, CHANNEL_KEY_FILE, password.encode()) imported = import_bundle(bundle, IDENTITY_DIR, CHANNEL_KEY_FILE, password.encode())
click.echo(f"Imported: {', '.join(imported.keys())}") click.echo(f"Imported: {', '.join(imported.keys())}")
@ -836,9 +837,9 @@ def chain():
pass pass
@chain.command() @chain.command("status")
@click.pass_context @click.pass_context
def status(ctx): def chain_status(ctx):
"""Show chain status — head index, chain ID, record count.""" """Show chain status — head index, chain ID, record count."""
from soosef.federation.chain import ChainStore from soosef.federation.chain import ChainStore
from soosef.paths import CHAIN_DIR from soosef.paths import CHAIN_DIR
@ -899,10 +900,10 @@ def verify():
raise SystemExit(1) raise SystemExit(1)
@chain.command() @chain.command("show")
@click.argument("index", type=int) @click.argument("index", type=int)
@click.pass_context @click.pass_context
def show(ctx, index): def chain_show(ctx, index):
"""Show a specific chain record by index.""" """Show a specific chain record by index."""
from soosef.exceptions import ChainError from soosef.exceptions import ChainError
from soosef.federation.chain import ChainStore from soosef.federation.chain import ChainStore

View File

@ -6,7 +6,7 @@ Config is intentionally simple — a flat JSON file with sensible defaults.
""" """
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from soosef.paths import CONFIG_FILE from soosef.paths import CONFIG_FILE

View File

@ -5,7 +5,7 @@ Killswitch, dead man's switch, tamper detection, USB monitoring.
All features are opt-in and disabled by default. All features are opt-in and disabled by default.
""" """
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
from soosef.fieldkit.deadman import DeadmanSwitch from soosef.fieldkit.deadman import DeadmanSwitch
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
__all__ = ["PurgeScope", "execute_purge", "DeadmanSwitch"] __all__ = ["PurgeScope", "execute_purge", "DeadmanSwitch"]

View File

@ -28,14 +28,14 @@ class GeoCircle:
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Distance in meters between two lat/lon points.""" """Distance in meters between two lat/lon points."""
R = 6371000 # Earth radius in meters earth_r = 6371000 # Earth radius in meters
phi1 = math.radians(lat1) phi1 = math.radians(lat1)
phi2 = math.radians(lat2) phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1) dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1) dlambda = math.radians(lon2 - lon1)
a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2 a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) return earth_r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def is_inside(fence: GeoCircle, lat: float, lon: float) -> bool: def is_inside(fence: GeoCircle, lat: float, lon: float) -> bool:

View File

@ -18,7 +18,6 @@ import subprocess
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from soosef.exceptions import KillswitchError
import soosef.paths as paths import soosef.paths as paths
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import os
from pathlib import Path from pathlib import Path
import pytest import pytest

View File

@ -203,9 +203,7 @@ def test_metadata_in_chain(chain_dir: Path, private_key: Ed25519PrivateKey):
"""Metadata is preserved through append and retrieval.""" """Metadata is preserved through append and retrieval."""
store = ChainStore(chain_dir) store = ChainStore(chain_dir)
meta = {"caption": "evidence photo", "backfilled": True} meta = {"caption": "evidence photo", "backfilled": True}
record = store.append( store.append(hashlib.sha256(b"test").digest(), "test/plain", private_key, metadata=meta)
hashlib.sha256(b"test").digest(), "test/plain", private_key, metadata=meta
)
loaded = store.get(0) loaded = store.get(0)
assert loaded.metadata == meta assert loaded.metadata == meta
@ -232,14 +230,16 @@ def test_verify_chain_detects_signer_change(chain_dir: Path):
# Manually bypass normal append to inject a record signed by key2. # Manually bypass normal append to inject a record signed by key2.
# We need to build the record with correct prev_hash but wrong signer. # We need to build the record with correct prev_hash but wrong signer.
import struct
import fcntl import fcntl
from soosef.federation.serialization import serialize_record import struct
from soosef.federation.models import AttestationChainRecord
from soosef.federation.entropy import collect_entropy_witnesses
from uuid_utils import uuid7
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from uuid_utils import uuid7
from soosef.federation.entropy import collect_entropy_witnesses
from soosef.federation.models import AttestationChainRecord
from soosef.federation.serialization import canonical_bytes as cb from soosef.federation.serialization import canonical_bytes as cb
from soosef.federation.serialization import serialize_record
state = store.state() state = store.state()
prev_hash = state.head_hash prev_hash = state.head_hash

View File

@ -15,8 +15,7 @@ import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from soosef.exceptions import ChainError from soosef.exceptions import ChainError
from soosef.federation.chain import ChainStore, MAX_RECORD_SIZE from soosef.federation.chain import MAX_RECORD_SIZE, ChainStore
from soosef.federation.serialization import compute_record_hash
def test_concurrent_append_no_fork(chain_dir: Path): def test_concurrent_append_no_fork(chain_dir: Path):

View File

@ -182,10 +182,9 @@ def test_enforcement_loop_tolerates_exceptions(tmp_path: Path, monkeypatch: pyte
def test_start_deadman_thread_is_daemon(monkeypatch: pytest.MonkeyPatch): def test_start_deadman_thread_is_daemon(monkeypatch: pytest.MonkeyPatch):
"""Thread must be a daemon so it dies with the process.""" """Thread must be a daemon so it dies with the process."""
from soosef.cli import _start_deadman_thread
# Patch the loop to exit immediately so the thread doesn't hang in tests # Patch the loop to exit immediately so the thread doesn't hang in tests
import soosef.cli as cli_mod import soosef.cli as cli_mod
from soosef.cli import _start_deadman_thread
monkeypatch.setattr(cli_mod, "_deadman_enforcement_loop", lambda interval_seconds: None) monkeypatch.setattr(cli_mod, "_deadman_enforcement_loop", lambda interval_seconds: None)
@ -208,8 +207,8 @@ def test_check_deadman_disarmed(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
): ):
"""check-deadman exits 0 and prints helpful message when not armed.""" """check-deadman exits 0 and prints helpful message when not armed."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
# Point at an empty tmp dir so the real ~/.soosef/fieldkit/deadman.json isn't read # Point at an empty tmp dir so the real ~/.soosef/fieldkit/deadman.json isn't read
state_file = tmp_path / "deadman.json" state_file = tmp_path / "deadman.json"
@ -224,8 +223,8 @@ def test_check_deadman_armed_ok(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
): ):
"""check-deadman exits 0 when armed and check-in is current.""" """check-deadman exits 0 when armed and check-in is current."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json" state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file) monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
@ -248,8 +247,8 @@ def test_check_deadman_overdue_in_grace(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
): ):
"""check-deadman exits 0 but prints OVERDUE warning when past interval but in grace.""" """check-deadman exits 0 but prints OVERDUE warning when past interval but in grace."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json" state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file) monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
@ -274,8 +273,8 @@ def test_check_deadman_fires_when_expired(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
): ):
"""check-deadman exits 2 when the switch has fully expired.""" """check-deadman exits 2 when the switch has fully expired."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main from soosef.cli import main
from soosef.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json" state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file) monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)