Fix black formatting and target Python 3.12 in CI
Reformat 8 files and add --target-version py312 to avoid 3.13 AST parsing issues with Python 3.12 container. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6d6a626b6b
commit
5c74a5f4aa
@ -18,7 +18,7 @@ jobs:
|
||||
git clone --depth=1 --branch="${GITHUB_REF_NAME}" https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE" || git clone --depth=1 https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE"
|
||||
- run: pip install ruff black
|
||||
- name: Check formatting
|
||||
run: black --check src/ tests/ frontends/
|
||||
run: black --check --target-version py312 src/ tests/ frontends/
|
||||
- name: Lint
|
||||
run: ruff check src/ tests/ frontends/
|
||||
|
||||
|
||||
@ -477,7 +477,9 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
detail=None if success else message,
|
||||
)
|
||||
if success:
|
||||
flash(f"User '{username}' created with temporary password: {temp_password}", "success")
|
||||
flash(
|
||||
f"User '{username}' created with temporary password: {temp_password}", "success"
|
||||
)
|
||||
else:
|
||||
flash(message, "error")
|
||||
return redirect(url_for("admin_users"))
|
||||
@ -535,7 +537,9 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
|
||||
if not use_pin and not use_rsa:
|
||||
flash("You must select at least one security factor (PIN or RSA Key)", "error")
|
||||
return render_template("stego/generate.html", generated=False, has_qrcode=_HAS_QRCODE)
|
||||
return render_template(
|
||||
"stego/generate.html", generated=False, has_qrcode=_HAS_QRCODE
|
||||
)
|
||||
|
||||
pin_length = int(request.form.get("pin_length", 6))
|
||||
rsa_bits = int(request.form.get("rsa_bits", 2048))
|
||||
@ -569,7 +573,11 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
temp_storage.save_temp_file(
|
||||
qr_token,
|
||||
creds.rsa_key_pem.encode(),
|
||||
{"filename": "rsa_key.pem", "type": "rsa_key", "compress": qr_needs_compression},
|
||||
{
|
||||
"filename": "rsa_key.pem",
|
||||
"type": "rsa_key",
|
||||
"compress": qr_needs_compression,
|
||||
},
|
||||
)
|
||||
|
||||
return render_template(
|
||||
@ -594,7 +602,9 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
)
|
||||
except Exception as e:
|
||||
flash(f"Error generating credentials: {e}", "error")
|
||||
return render_template("stego/generate.html", generated=False, has_qrcode=_HAS_QRCODE)
|
||||
return render_template(
|
||||
"stego/generate.html", generated=False, has_qrcode=_HAS_QRCODE
|
||||
)
|
||||
|
||||
return render_template("stego/generate.html", generated=False, has_qrcode=_HAS_QRCODE)
|
||||
|
||||
@ -633,8 +643,10 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
compress = file_info.get("compress", False)
|
||||
qr_png = generate_qr_code(key_pem, compress=compress)
|
||||
return send_file(
|
||||
io.BytesIO(qr_png), mimetype="image/png",
|
||||
as_attachment=True, download_name="soosef_rsa_key_qr.png",
|
||||
io.BytesIO(qr_png),
|
||||
mimetype="image/png",
|
||||
as_attachment=True,
|
||||
download_name="soosef_rsa_key_qr.png",
|
||||
)
|
||||
except Exception as e:
|
||||
return f"Error generating QR code: {e}", 500
|
||||
@ -656,8 +668,10 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
key_id = secrets.token_hex(4)
|
||||
filename = f"soosef_key_{private_key.key_size}_{key_id}.pem"
|
||||
return send_file(
|
||||
io.BytesIO(encrypted_pem), mimetype="application/x-pem-file",
|
||||
as_attachment=True, download_name=filename,
|
||||
io.BytesIO(encrypted_pem),
|
||||
mimetype="application/x-pem-file",
|
||||
as_attachment=True,
|
||||
download_name=filename,
|
||||
)
|
||||
except Exception as e:
|
||||
flash(f"Error creating key file: {e}", "error")
|
||||
@ -667,12 +681,15 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
|
||||
from stego_routes import register_stego_routes
|
||||
|
||||
register_stego_routes(app, **{
|
||||
"login_required": login_required,
|
||||
"subprocess_stego": subprocess_stego,
|
||||
"temp_storage": temp_storage,
|
||||
"has_qrcode_read": _HAS_QRCODE_READ,
|
||||
})
|
||||
register_stego_routes(
|
||||
app,
|
||||
**{
|
||||
"login_required": login_required,
|
||||
"subprocess_stego": subprocess_stego,
|
||||
"temp_storage": temp_storage,
|
||||
"has_qrcode_read": _HAS_QRCODE_READ,
|
||||
},
|
||||
)
|
||||
|
||||
# /about route is in stego_routes.py
|
||||
|
||||
@ -683,22 +700,26 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
def api_channel_status():
|
||||
result = subprocess_stego.get_channel_status(reveal=False)
|
||||
if result.success:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"mode": result.mode,
|
||||
"configured": result.configured,
|
||||
"fingerprint": result.fingerprint,
|
||||
"source": result.source,
|
||||
})
|
||||
return jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"mode": result.mode,
|
||||
"configured": result.configured,
|
||||
"fingerprint": result.fingerprint,
|
||||
"source": result.source,
|
||||
}
|
||||
)
|
||||
else:
|
||||
status = get_channel_status()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"mode": status["mode"],
|
||||
"configured": status["configured"],
|
||||
"fingerprint": status.get("fingerprint"),
|
||||
"source": status.get("source"),
|
||||
})
|
||||
return jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"mode": status["mode"],
|
||||
"configured": status["configured"],
|
||||
"fingerprint": status.get("fingerprint"),
|
||||
"source": status.get("source"),
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/compare-capacity", methods=["POST"])
|
||||
@login_required
|
||||
@ -711,23 +732,25 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
result = subprocess_stego.compare_modes(carrier_data)
|
||||
if not result.success:
|
||||
return jsonify({"error": result.error or "Comparison failed"}), 500
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"width": result.width,
|
||||
"height": result.height,
|
||||
"lsb": {
|
||||
"capacity_bytes": result.lsb["capacity_bytes"],
|
||||
"capacity_kb": round(result.lsb["capacity_kb"], 1),
|
||||
"output": result.lsb.get("output", "PNG"),
|
||||
},
|
||||
"dct": {
|
||||
"capacity_bytes": result.dct["capacity_bytes"],
|
||||
"capacity_kb": round(result.dct["capacity_kb"], 1),
|
||||
"output": result.dct.get("output", "JPEG"),
|
||||
"available": result.dct.get("available", True),
|
||||
"ratio": round(result.dct.get("ratio_vs_lsb", 0), 1),
|
||||
},
|
||||
})
|
||||
return jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"width": result.width,
|
||||
"height": result.height,
|
||||
"lsb": {
|
||||
"capacity_bytes": result.lsb["capacity_bytes"],
|
||||
"capacity_kb": round(result.lsb["capacity_kb"], 1),
|
||||
"output": result.lsb.get("output", "PNG"),
|
||||
},
|
||||
"dct": {
|
||||
"capacity_bytes": result.dct["capacity_bytes"],
|
||||
"capacity_kb": round(result.dct["capacity_kb"], 1),
|
||||
"output": result.dct.get("output", "JPEG"),
|
||||
"available": result.dct.get("available", True),
|
||||
"ratio": round(result.dct.get("ratio_vs_lsb", 0), 1),
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@ -751,11 +774,13 @@ def _register_stegasoo_routes(app: Flask) -> None:
|
||||
def api_generate_credentials():
|
||||
try:
|
||||
creds = generate_credentials(use_pin=True, use_rsa=False)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"passphrase": creds.passphrase,
|
||||
"pin": creds.pin,
|
||||
})
|
||||
return jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"passphrase": creds.passphrase,
|
||||
"pin": creds.pin,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@ -95,7 +95,9 @@ def register_stego_routes(app, **deps):
|
||||
def _cleanup_old_jobs(max_age_seconds=3600):
|
||||
now = time.time()
|
||||
with _jobs_lock:
|
||||
to_remove = [jid for jid, data in _jobs.items() if now - data.get("created", 0) > max_age_seconds]
|
||||
to_remove = [
|
||||
jid for jid, data in _jobs.items() if now - data.get("created", 0) > max_age_seconds
|
||||
]
|
||||
for jid in to_remove:
|
||||
cleanup_progress_file(jid)
|
||||
del _jobs[jid]
|
||||
@ -112,7 +114,8 @@ def register_stego_routes(app, **deps):
|
||||
with Image.open(io.BytesIO(image_data)) as img:
|
||||
if img.mode in ("RGBA", "LA", "P"):
|
||||
bg = Image.new("RGB", img.size, (255, 255, 255))
|
||||
if img.mode == "P": img = img.convert("RGBA")
|
||||
if img.mode == "P":
|
||||
img = img.convert("RGBA")
|
||||
bg.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None)
|
||||
img = bg
|
||||
elif img.mode != "RGB":
|
||||
@ -128,15 +131,27 @@ def register_stego_routes(app, **deps):
|
||||
temp_storage.cleanup_expired(TEMP_FILE_EXPIRY)
|
||||
|
||||
def allowed_image(fn):
|
||||
return bool(fn and "." in fn and fn.rsplit(".", 1)[1].lower() in {"png", "jpg", "jpeg", "bmp", "gif"})
|
||||
return bool(
|
||||
fn
|
||||
and "." in fn
|
||||
and fn.rsplit(".", 1)[1].lower() in {"png", "jpg", "jpeg", "bmp", "gif"}
|
||||
)
|
||||
|
||||
def allowed_audio(fn):
|
||||
return bool(fn and "." in fn and fn.rsplit(".", 1)[1].lower() in {"wav", "flac", "mp3", "ogg", "aac", "m4a", "aiff", "aif"})
|
||||
return bool(
|
||||
fn
|
||||
and "." in fn
|
||||
and fn.rsplit(".", 1)[1].lower()
|
||||
in {"wav", "flac", "mp3", "ogg", "aac", "m4a", "aiff", "aif"}
|
||||
)
|
||||
|
||||
def format_size(n):
|
||||
if n < 1024: return f"{n} B"
|
||||
elif n < 1024*1024: return f"{n/1024:.1f} KB"
|
||||
else: return f"{n/(1024*1024):.1f} MB"
|
||||
if n < 1024:
|
||||
return f"{n} B"
|
||||
elif n < 1024 * 1024:
|
||||
return f"{n/1024:.1f} KB"
|
||||
else:
|
||||
return f"{n/(1024*1024):.1f} MB"
|
||||
|
||||
# ── Routes below are extracted from stegasoo app.py ──
|
||||
|
||||
@ -247,7 +262,6 @@ def register_stego_routes(app, **deps):
|
||||
finally:
|
||||
cleanup_progress_file(job_id)
|
||||
|
||||
|
||||
def _run_encode_audio_job(job_id: str, encode_params: dict) -> None:
|
||||
"""Background thread function for async audio encode (v4.3.0)."""
|
||||
progress_file = get_progress_file_path(job_id)
|
||||
@ -333,13 +347,14 @@ def register_stego_routes(app, **deps):
|
||||
finally:
|
||||
cleanup_progress_file(job_id)
|
||||
|
||||
|
||||
@app.route("/encode", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def encode():
|
||||
if request.method == "POST":
|
||||
# Check if async mode requested
|
||||
is_async = request.form.get("async") == "true" or request.headers.get("X-Async") == "true"
|
||||
is_async = (
|
||||
request.form.get("async") == "true" or request.headers.get("X-Async") == "true"
|
||||
)
|
||||
|
||||
def _error_response(msg):
|
||||
"""Return error as JSON (async) or HTML flash (sync)."""
|
||||
@ -366,7 +381,9 @@ def register_stego_routes(app, **deps):
|
||||
)
|
||||
|
||||
if not ref_photo or not carrier:
|
||||
return _error_response("Both reference photo and audio carrier are required")
|
||||
return _error_response(
|
||||
"Both reference photo and audio carrier are required"
|
||||
)
|
||||
|
||||
if not allowed_image(ref_photo.filename):
|
||||
return _error_response("Reference must be an image (PNG, JPG, BMP)")
|
||||
@ -458,7 +475,9 @@ def register_stego_routes(app, **deps):
|
||||
if not result.is_valid:
|
||||
return _error_response(result.error_message)
|
||||
|
||||
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
||||
key_password = (
|
||||
None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
||||
)
|
||||
|
||||
if rsa_key_data:
|
||||
result = validate_rsa_key(rsa_key_data, key_password)
|
||||
@ -578,7 +597,9 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
# Check DCT availability
|
||||
if embed_mode == "dct" and not has_dct_support():
|
||||
return _error_response("DCT mode requires scipy. Install with: pip install scipy")
|
||||
return _error_response(
|
||||
"DCT mode requires scipy. Install with: pip install scipy"
|
||||
)
|
||||
|
||||
# Determine payload
|
||||
if payload_type == "file" and payload_file and payload_file.filename:
|
||||
@ -764,7 +785,11 @@ def register_stego_routes(app, **deps):
|
||||
filename = encode_result.filename
|
||||
if not filename:
|
||||
filename = generate_filename("stego", output_ext)
|
||||
elif embed_mode == "dct" and dct_output_format == "jpeg" and filename.endswith(".png"):
|
||||
elif (
|
||||
embed_mode == "dct"
|
||||
and dct_output_format == "jpeg"
|
||||
and filename.endswith(".png")
|
||||
):
|
||||
filename = filename[:-4] + ".jpg"
|
||||
|
||||
# Store temporarily
|
||||
@ -796,12 +821,10 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
return render_template("stego/encode.html", has_qrcode_read=_HAS_QRCODE_READ)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ENCODE PROGRESS ENDPOINTS (v4.1.2)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@app.route("/encode/status/<job_id>")
|
||||
@login_required
|
||||
def encode_status(job_id):
|
||||
@ -819,7 +842,6 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@app.route("/encode/progress/<job_id>")
|
||||
@login_required
|
||||
def encode_progress(job_id):
|
||||
@ -843,7 +865,6 @@ def register_stego_routes(app, **deps):
|
||||
# Running but no progress file yet
|
||||
return jsonify({"percent": 0, "phase": "initializing"})
|
||||
|
||||
|
||||
@app.route("/encode/result/<file_id>")
|
||||
@login_required
|
||||
def encode_result(file_id):
|
||||
@ -867,7 +888,9 @@ def register_stego_routes(app, **deps):
|
||||
"encode_result.html",
|
||||
file_id=file_id,
|
||||
filename=file_info["filename"],
|
||||
thumbnail_url=url_for("encode_thumbnail", thumb_id=thumbnail_id) if thumbnail_id else None,
|
||||
thumbnail_url=(
|
||||
url_for("encode_thumbnail", thumb_id=thumbnail_id) if thumbnail_id else None
|
||||
),
|
||||
embed_mode=file_info.get("embed_mode", "lsb"),
|
||||
output_format=file_info.get("output_format", "png"),
|
||||
color_mode=file_info.get("color_mode"),
|
||||
@ -877,7 +900,6 @@ def register_stego_routes(app, **deps):
|
||||
channel_fingerprint=file_info.get("channel_fingerprint"),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/encode/thumbnail/<thumb_id>")
|
||||
@login_required
|
||||
def encode_thumbnail(thumb_id):
|
||||
@ -888,7 +910,6 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
return send_file(io.BytesIO(thumb_data), mimetype="image/jpeg", as_attachment=False)
|
||||
|
||||
|
||||
@app.route("/encode/download/<file_id>")
|
||||
@login_required
|
||||
def encode_download(file_id):
|
||||
@ -906,7 +927,6 @@ def register_stego_routes(app, **deps):
|
||||
download_name=file_info["filename"],
|
||||
)
|
||||
|
||||
|
||||
@app.route("/encode/file/<file_id>")
|
||||
@login_required
|
||||
def encode_file_route(file_id):
|
||||
@ -924,7 +944,6 @@ def register_stego_routes(app, **deps):
|
||||
download_name=file_info["filename"],
|
||||
)
|
||||
|
||||
|
||||
@app.route("/encode/cleanup/<file_id>", methods=["POST"])
|
||||
@login_required
|
||||
def encode_cleanup(file_id):
|
||||
@ -937,12 +956,10 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DECODE
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _run_decode_job(job_id: str, decode_params: dict) -> None:
|
||||
"""Background thread function for async decode."""
|
||||
progress_file = get_progress_file_path(job_id)
|
||||
@ -1022,7 +1039,6 @@ def register_stego_routes(app, **deps):
|
||||
finally:
|
||||
cleanup_progress_file(job_id)
|
||||
|
||||
|
||||
def _run_decode_audio_job(job_id: str, decode_params: dict) -> None:
|
||||
"""Background thread function for async audio decode (v4.3.0)."""
|
||||
progress_file = get_progress_file_path(job_id)
|
||||
@ -1100,7 +1116,6 @@ def register_stego_routes(app, **deps):
|
||||
finally:
|
||||
cleanup_progress_file(job_id)
|
||||
|
||||
|
||||
@app.route("/decode", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def decode_page():
|
||||
@ -1118,19 +1133,27 @@ def register_stego_routes(app, **deps):
|
||||
# ========== AUDIO DECODE PATH (v4.3.0) ==========
|
||||
if not HAS_AUDIO_SUPPORT:
|
||||
flash("Audio steganography is not available.", "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
|
||||
)
|
||||
|
||||
if not ref_photo or not stego_image:
|
||||
flash("Both reference photo and stego audio 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
|
||||
)
|
||||
|
||||
if not allowed_image(ref_photo.filename):
|
||||
flash("Reference must be an image", "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
|
||||
)
|
||||
|
||||
if not allowed_audio(stego_image.filename):
|
||||
flash("Invalid audio format", "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
|
||||
)
|
||||
|
||||
passphrase = request.form.get("passphrase", "")
|
||||
pin = request.form.get("pin", "").strip()
|
||||
@ -1144,7 +1167,9 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
if not passphrase:
|
||||
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
|
||||
)
|
||||
|
||||
ref_data = ref_photo.read()
|
||||
stego_data = stego_image.read()
|
||||
@ -1170,29 +1195,40 @@ def register_stego_routes(app, **deps):
|
||||
rsa_key_from_qr = True
|
||||
else:
|
||||
flash("Could not extract RSA key from QR code image.", "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
|
||||
)
|
||||
|
||||
result = validate_security_factors(pin, rsa_key_data)
|
||||
if not result.is_valid:
|
||||
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
|
||||
)
|
||||
|
||||
if pin:
|
||||
result = validate_pin(pin)
|
||||
if not result.is_valid:
|
||||
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
|
||||
)
|
||||
|
||||
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
||||
key_password = (
|
||||
None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
||||
)
|
||||
|
||||
if rsa_key_data:
|
||||
result = validate_rsa_key(rsa_key_data, key_password)
|
||||
if not result.is_valid:
|
||||
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
|
||||
)
|
||||
|
||||
is_async = (
|
||||
request.form.get("async") == "true" or request.headers.get("X-Async") == "true"
|
||||
request.form.get("async") == "true"
|
||||
or request.headers.get("X-Async") == "true"
|
||||
)
|
||||
|
||||
decode_params = {
|
||||
@ -1237,7 +1273,9 @@ def register_stego_routes(app, **deps):
|
||||
)
|
||||
else:
|
||||
flash(error_msg, "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
|
||||
)
|
||||
|
||||
if decode_result.is_file:
|
||||
file_id = secrets.token_urlsafe(16)
|
||||
@ -1323,7 +1361,9 @@ def register_stego_routes(app, **deps):
|
||||
rsa_key_from_qr = True
|
||||
else:
|
||||
flash("Could not extract RSA key from QR code image.", "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 security factors
|
||||
result = validate_security_factors(pin, rsa_key_data)
|
||||
@ -1336,7 +1376,9 @@ def register_stego_routes(app, **deps):
|
||||
result = validate_pin(pin)
|
||||
if not result.is_valid:
|
||||
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
|
||||
)
|
||||
|
||||
# Determine key password
|
||||
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
||||
@ -1346,7 +1388,9 @@ def register_stego_routes(app, **deps):
|
||||
result = validate_rsa_key(rsa_key_data, key_password)
|
||||
if not result.is_valid:
|
||||
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
|
||||
)
|
||||
|
||||
# Check for async mode (v4.1.5)
|
||||
is_async = (
|
||||
@ -1392,8 +1436,13 @@ def register_stego_routes(app, **deps):
|
||||
# Check for channel key related errors
|
||||
if "channel key" in error_msg.lower():
|
||||
flash(error_msg, "error")
|
||||
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ)
|
||||
if "decrypt" in error_msg.lower() or decode_result.error_type == "DecryptionError":
|
||||
return render_template(
|
||||
"stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ
|
||||
)
|
||||
if (
|
||||
"decrypt" in error_msg.lower()
|
||||
or decode_result.error_type == "DecryptionError"
|
||||
):
|
||||
raise DecryptionError(error_msg)
|
||||
raise StegasooError(error_msg)
|
||||
|
||||
@ -1462,7 +1511,6 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
return render_template("stego/decode.html", has_qrcode_read=_HAS_QRCODE_READ)
|
||||
|
||||
|
||||
@app.route("/decode/download/<file_id>")
|
||||
@login_required
|
||||
def decode_download(file_id):
|
||||
@ -1481,12 +1529,10 @@ def register_stego_routes(app, **deps):
|
||||
download_name=file_info["filename"],
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DECODE PROGRESS ENDPOINTS (v4.1.5)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@app.route("/decode/status/<job_id>")
|
||||
@login_required
|
||||
def decode_status(job_id):
|
||||
@ -1512,7 +1558,6 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@app.route("/decode/progress/<job_id>")
|
||||
@login_required
|
||||
def decode_progress(job_id):
|
||||
@ -1536,7 +1581,6 @@ def register_stego_routes(app, **deps):
|
||||
# Running but no progress file yet
|
||||
return jsonify({"percent": 5, "phase": "reading"})
|
||||
|
||||
|
||||
@app.route("/decode/result/<job_id>")
|
||||
@login_required
|
||||
def decode_result(job_id):
|
||||
@ -1567,7 +1611,6 @@ def register_stego_routes(app, **deps):
|
||||
has_qrcode_read=_HAS_QRCODE_READ,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/about")
|
||||
def about():
|
||||
from stegasoo.channel import get_channel_status
|
||||
@ -1588,19 +1631,16 @@ def register_stego_routes(app, **deps):
|
||||
is_admin=is_admin_user,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOLS ROUTES (v4.1.0)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@app.route("/tools")
|
||||
@login_required
|
||||
def tools():
|
||||
"""Advanced tools page."""
|
||||
return render_template("stego/tools.html", has_dct=has_dct_support())
|
||||
|
||||
|
||||
@app.route("/api/tools/capacity", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_capacity():
|
||||
@ -1621,7 +1661,6 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/tools/strip-metadata", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_strip_metadata():
|
||||
@ -1641,11 +1680,12 @@ def register_stego_routes(app, **deps):
|
||||
buffer = io.BytesIO(clean_data)
|
||||
filename = image_file.filename.rsplit(".", 1)[0] + "_clean.png"
|
||||
|
||||
return send_file(buffer, mimetype="image/png", as_attachment=True, download_name=filename)
|
||||
return send_file(
|
||||
buffer, mimetype="image/png", as_attachment=True, download_name=filename
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/tools/exif", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_exif():
|
||||
@ -1675,7 +1715,6 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/tools/exif/update", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_exif_update():
|
||||
@ -1715,7 +1754,6 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/tools/exif/clear", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_exif_clear():
|
||||
@ -1759,7 +1797,6 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/tools/rotate", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_rotate():
|
||||
@ -1786,7 +1823,9 @@ def register_stego_routes(app, **deps):
|
||||
|
||||
# For JPEGs, use jpegtran for lossless rotation/flip (preserves DCT stego)
|
||||
has_jpegtran = shutil.which("jpegtran") is not None
|
||||
use_jpegtran = original_format == "JPEG" and has_jpegtran and (rotation or flip_h or flip_v)
|
||||
use_jpegtran = (
|
||||
original_format == "JPEG" and has_jpegtran and (rotation or flip_h or flip_v)
|
||||
)
|
||||
|
||||
if use_jpegtran:
|
||||
# Chain jpegtran operations for lossless transformation
|
||||
@ -1930,7 +1969,6 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/tools/compress", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_compress():
|
||||
@ -1969,7 +2007,6 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/tools/convert", methods=["POST"])
|
||||
@login_required
|
||||
def api_tools_convert():
|
||||
@ -2022,10 +2059,8 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
# Add these two test routes anywhere in app.py after the app = Flask(...) line:
|
||||
|
||||
|
||||
@app.route("/test-capacity", methods=["POST"])
|
||||
def test_capacity():
|
||||
"""Minimal capacity test - no stegasoo code, just PIL."""
|
||||
@ -2059,7 +2094,6 @@ def register_stego_routes(app, **deps):
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/test-capacity-nopil", methods=["POST"])
|
||||
def test_capacity_nopil():
|
||||
"""Ultra-minimal test - no PIL, no stegasoo."""
|
||||
@ -2075,8 +2109,6 @@ def register_stego_routes(app, **deps):
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATION ROUTES (v4.0.2)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@ -433,9 +433,7 @@ class ChainStore:
|
||||
if prev_record is not None:
|
||||
expected_hash = compute_record_hash(prev_record)
|
||||
if record.prev_hash != expected_hash:
|
||||
raise ChainIntegrityError(
|
||||
f"Record {record.chain_index}: prev_hash mismatch"
|
||||
)
|
||||
raise ChainIntegrityError(f"Record {record.chain_index}: prev_hash mismatch")
|
||||
elif record.chain_index == 0:
|
||||
if record.prev_hash != ChainState.GENESIS_PREV_HASH:
|
||||
raise ChainIntegrityError("Genesis record has non-zero prev_hash")
|
||||
|
||||
@ -47,9 +47,7 @@ class DeadmanSwitch:
|
||||
state["grace_hours"] = grace_hours
|
||||
state["last_checkin"] = datetime.now(UTC).isoformat()
|
||||
self._save_state(state)
|
||||
logger.info(
|
||||
"Dead man's switch armed: %dh interval, %dh grace", interval_hours, grace_hours
|
||||
)
|
||||
logger.info("Dead man's switch armed: %dh interval, %dh grace", interval_hours, grace_hours)
|
||||
|
||||
def disarm(self) -> None:
|
||||
"""Disarm the dead man's switch."""
|
||||
@ -83,9 +81,7 @@ class DeadmanSwitch:
|
||||
if not state["armed"] or not state["last_checkin"]:
|
||||
return False
|
||||
last = datetime.fromisoformat(state["last_checkin"])
|
||||
deadline = last + timedelta(
|
||||
hours=state["interval_hours"] + state["grace_hours"]
|
||||
)
|
||||
deadline = last + timedelta(hours=state["interval_hours"] + state["grace_hours"])
|
||||
return datetime.now(UTC) > deadline
|
||||
|
||||
def status(self) -> dict:
|
||||
|
||||
@ -92,14 +92,16 @@ def execute_purge(scope: PurgeScope = PurgeScope.ALL, reason: str = "manual") ->
|
||||
]
|
||||
|
||||
if scope == PurgeScope.ALL:
|
||||
steps.extend([
|
||||
("destroy_auth_db", lambda: _secure_delete_file(paths.AUTH_DB)),
|
||||
("destroy_attestation_log", lambda: _secure_delete_dir(paths.ATTESTATIONS_DIR)),
|
||||
("destroy_chain_data", lambda: _secure_delete_dir(paths.CHAIN_DIR)),
|
||||
("destroy_temp_files", lambda: _secure_delete_dir(paths.TEMP_DIR)),
|
||||
("destroy_config", lambda: _secure_delete_file(paths.CONFIG_FILE)),
|
||||
("clear_journald", _clear_system_logs),
|
||||
])
|
||||
steps.extend(
|
||||
[
|
||||
("destroy_auth_db", lambda: _secure_delete_file(paths.AUTH_DB)),
|
||||
("destroy_attestation_log", lambda: _secure_delete_dir(paths.ATTESTATIONS_DIR)),
|
||||
("destroy_chain_data", lambda: _secure_delete_dir(paths.CHAIN_DIR)),
|
||||
("destroy_temp_files", lambda: _secure_delete_dir(paths.TEMP_DIR)),
|
||||
("destroy_config", lambda: _secure_delete_file(paths.CONFIG_FILE)),
|
||||
("clear_journald", _clear_system_logs),
|
||||
]
|
||||
)
|
||||
|
||||
for name, action in steps:
|
||||
try:
|
||||
|
||||
@ -135,9 +135,7 @@ class KeystoreManager:
|
||||
from datetime import UTC, datetime
|
||||
|
||||
meta_path = self._identity_meta_path()
|
||||
meta_path.write_text(
|
||||
json.dumps({"created_at": datetime.now(UTC).isoformat()}, indent=None)
|
||||
)
|
||||
meta_path.write_text(json.dumps({"created_at": datetime.now(UTC).isoformat()}, indent=None))
|
||||
|
||||
return self.get_identity()
|
||||
|
||||
@ -263,8 +261,7 @@ class KeystoreManager:
|
||||
from datetime import UTC, datetime
|
||||
|
||||
(archive_dir / "rotation.txt").write_text(
|
||||
f"Rotated at: {datetime.now(UTC).isoformat()}\n"
|
||||
f"Old fingerprint: {old_fp}\n"
|
||||
f"Rotated at: {datetime.now(UTC).isoformat()}\n" f"Old fingerprint: {old_fp}\n"
|
||||
)
|
||||
|
||||
new_key = self.generate_channel_key()
|
||||
|
||||
@ -52,8 +52,7 @@ def test_concurrent_append_no_fork(chain_dir: Path):
|
||||
|
||||
# Every index must be unique (no fork)
|
||||
assert len(all_indices) == len(set(all_indices)), (
|
||||
f"Duplicate chain indices detected — chain forked! "
|
||||
f"Indices: {sorted(all_indices)}"
|
||||
f"Duplicate chain indices detected — chain forked! " f"Indices: {sorted(all_indices)}"
|
||||
)
|
||||
|
||||
# Indices should be 0..N-1 contiguous
|
||||
@ -100,7 +99,7 @@ def test_truncated_chain_file(chain_dir: Path, private_key: Ed25519PrivateKey):
|
||||
# Truncate the file mid-record
|
||||
chain_file = chain_dir / "chain.bin"
|
||||
data = chain_file.read_bytes()
|
||||
chain_file.write_bytes(data[:len(data) - 50])
|
||||
chain_file.write_bytes(data[: len(data) - 50])
|
||||
|
||||
store2 = ChainStore(chain_dir)
|
||||
records = list(store2._iter_raw())
|
||||
|
||||
@ -22,7 +22,6 @@ from pathlib import Path
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
||||
# ── Fixtures ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -205,7 +204,9 @@ def cli_runner():
|
||||
return CliRunner()
|
||||
|
||||
|
||||
def test_check_deadman_disarmed(tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_check_deadman_disarmed(
|
||||
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""check-deadman exits 0 and prints helpful message when not armed."""
|
||||
from soosef.fieldkit import deadman as deadman_mod
|
||||
from soosef.cli import main
|
||||
@ -219,7 +220,9 @@ def test_check_deadman_disarmed(tmp_path: Path, cli_runner: CliRunner, monkeypat
|
||||
assert "not armed" in result.output
|
||||
|
||||
|
||||
def test_check_deadman_armed_ok(tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_check_deadman_armed_ok(
|
||||
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""check-deadman exits 0 when armed and check-in is current."""
|
||||
from soosef.fieldkit import deadman as deadman_mod
|
||||
from soosef.cli import main
|
||||
@ -241,7 +244,9 @@ def test_check_deadman_armed_ok(tmp_path: Path, cli_runner: CliRunner, monkeypat
|
||||
assert "OK" in result.output
|
||||
|
||||
|
||||
def test_check_deadman_overdue_in_grace(tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_check_deadman_overdue_in_grace(
|
||||
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user