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