Implement 7 real-world scenario features (Round 4)
1. Source drop box: token-gated anonymous upload with auto-attestation, EXIF stripping, receipt codes, and self-destructing URLs. New /dropbox blueprint with admin panel for token management. CSRF exempted for source-facing upload routes. 2. Investigation namespaces: attestation records tagged with investigation label via metadata. Log view filters by investigation with dropdown. Supports long-running multi-story workflows. 3. Scale fixes: replaced O(n) full-scan perceptual hash search with LMDB find_similar_images() index lookup. Added incremental chain verification (verify_incremental) with last_verified_index checkpoint in ChainState. 4. Deep forensic purge: killswitch now scrubs __pycache__, pip dist-info, pip cache, and shell history entries containing 'soosef'. Runs before package uninstall for maximum trace removal. 5. Cross-org federation: new federation/exchange.py with export_attestation_bundle() and import_attestation_bundle(). Bundles are self-authenticating JSON with investigation filter. Import validates against trust store fingerprints. 6. Wrong-key diagnostics: enhanced decrypt error messages include current channel key fingerprint hint. New carrier_tracker.py tracks carrier SHA-256 hashes and warns on reuse (statistical analysis risk). 7. Selective disclosure: ChainStore.selective_disclosure() produces proof bundles with full selected records + hash-only redacted records + complete hash chain for linkage verification. New `soosef chain disclose -i 0,5,10 -o proof.json` CLI command for court-ordered evidence production. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,10 +132,13 @@ def attest():
|
||||
metadata = {}
|
||||
caption = request.form.get("caption", "").strip()
|
||||
location_name = request.form.get("location_name", "").strip()
|
||||
investigation = request.form.get("investigation", "").strip()
|
||||
if caption:
|
||||
metadata["caption"] = caption
|
||||
if location_name:
|
||||
metadata["location_name"] = location_name
|
||||
if investigation:
|
||||
metadata["investigation"] = investigation
|
||||
|
||||
auto_exif = request.form.get("auto_exif", "on") == "on"
|
||||
|
||||
@@ -358,15 +361,15 @@ def _verify_image(image_data: bytes) -> dict:
|
||||
for record in exact_records:
|
||||
matches.append({"record": record, "match_type": "exact", "distances": {}})
|
||||
|
||||
# Perceptual fallback
|
||||
# Perceptual fallback via LMDB index (O(index) not O(n) full scan)
|
||||
if not matches and query_hashes.phash:
|
||||
all_records = [storage.get_record(i) for i in range(stats.record_count)]
|
||||
for record in all_records:
|
||||
similar = storage.find_similar_images(query_hashes.phash, max_distance=10)
|
||||
for record, distance in similar:
|
||||
distances = compute_all_distances(query_hashes, record.image_hashes)
|
||||
same, match_type = is_same_image(
|
||||
query_hashes, record.image_hashes, perceptual_threshold=10
|
||||
)
|
||||
if same:
|
||||
distances = compute_all_distances(query_hashes, record.image_hashes)
|
||||
matches.append(
|
||||
{
|
||||
"record": record,
|
||||
@@ -588,20 +591,44 @@ def verify_receipt():
|
||||
@bp.route("/attest/log")
|
||||
@login_required
|
||||
def log():
|
||||
"""List recent attestations."""
|
||||
"""List recent attestations with optional investigation filter."""
|
||||
investigation_filter = request.args.get("investigation", "").strip()
|
||||
try:
|
||||
storage = _get_storage()
|
||||
stats = storage.get_stats()
|
||||
records = []
|
||||
# Show last 50 records, newest first
|
||||
start = max(0, stats.record_count - 50)
|
||||
for i in range(stats.record_count - 1, start - 1, -1):
|
||||
# Scan records, newest first, collect up to 50 matching
|
||||
for i in range(stats.record_count - 1, -1, -1):
|
||||
if len(records) >= 50:
|
||||
break
|
||||
try:
|
||||
record = storage.get_record(i)
|
||||
if investigation_filter:
|
||||
rec_inv = getattr(record, "metadata", {}) or {}
|
||||
if isinstance(rec_inv, dict) and rec_inv.get("investigation") != investigation_filter:
|
||||
continue
|
||||
records.append({"index": i, "record": record})
|
||||
except Exception:
|
||||
continue
|
||||
return render_template("attest/log.html", records=records, total=stats.record_count)
|
||||
|
||||
# Collect known investigation names for filter dropdown
|
||||
investigations = set()
|
||||
for i in range(stats.record_count - 1, max(0, stats.record_count - 500) - 1, -1):
|
||||
try:
|
||||
rec = storage.get_record(i)
|
||||
meta = getattr(rec, "metadata", {}) or {}
|
||||
if isinstance(meta, dict) and meta.get("investigation"):
|
||||
investigations.add(meta["investigation"])
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return render_template(
|
||||
"attest/log.html",
|
||||
records=records,
|
||||
total=stats.record_count,
|
||||
investigation_filter=investigation_filter,
|
||||
investigations=sorted(investigations),
|
||||
)
|
||||
except Exception as e:
|
||||
flash(f"Could not read attestation log: {e}", "error")
|
||||
return render_template("attest/log.html", records=[], total=0)
|
||||
return render_template("attest/log.html", records=[], total=0, investigation_filter="", investigations=[])
|
||||
|
||||
226
frontends/web/blueprints/dropbox.py
Normal file
226
frontends/web/blueprints/dropbox.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Source drop box blueprint — anonymous, token-gated file submission.
|
||||
|
||||
Provides a SecureDrop-like intake that lives inside SooSeF:
|
||||
- Admin creates a time-limited upload token
|
||||
- Source opens the token URL in a browser (no account needed)
|
||||
- Files are uploaded, EXIF-stripped, and auto-attested on receipt
|
||||
- Source receives a one-time receipt code to confirm delivery
|
||||
- Token self-destructs after use or timeout
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from auth import admin_required, login_required
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
|
||||
|
||||
from soosef.audit import log_action
|
||||
from soosef.paths import TEMP_DIR
|
||||
|
||||
bp = Blueprint("dropbox", __name__, url_prefix="/dropbox")
|
||||
|
||||
# In-memory token store. In production, this should be persisted to SQLite.
|
||||
# Token format: {token: {created_at, expires_at, max_files, label, used, receipts[]}}
|
||||
_tokens: dict[str, dict] = {}
|
||||
_TOKEN_DIR = TEMP_DIR / "dropbox"
|
||||
|
||||
|
||||
def _ensure_token_dir():
|
||||
_TOKEN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_TOKEN_DIR.chmod(0o700)
|
||||
|
||||
|
||||
@bp.route("/admin", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def admin():
|
||||
"""Admin panel for creating and managing drop box tokens."""
|
||||
if request.method == "POST":
|
||||
action = request.form.get("action")
|
||||
if action == "create":
|
||||
label = request.form.get("label", "").strip() or "Unnamed source"
|
||||
hours = int(request.form.get("hours", 24))
|
||||
max_files = int(request.form.get("max_files", 10))
|
||||
|
||||
token = secrets.token_urlsafe(32)
|
||||
_tokens[token] = {
|
||||
"created_at": datetime.now(UTC).isoformat(),
|
||||
"expires_at": (datetime.now(UTC) + timedelta(hours=hours)).isoformat(),
|
||||
"max_files": max_files,
|
||||
"label": label,
|
||||
"used": 0,
|
||||
"receipts": [],
|
||||
}
|
||||
|
||||
log_action(
|
||||
actor=request.environ.get("REMOTE_USER", "admin"),
|
||||
action="dropbox.token_created",
|
||||
target=token[:8],
|
||||
outcome="success",
|
||||
source="web",
|
||||
)
|
||||
|
||||
upload_url = url_for("dropbox.upload", token=token, _external=True)
|
||||
flash(f"Drop box created. Share this URL with your source: {upload_url}", "success")
|
||||
|
||||
elif action == "revoke":
|
||||
token = request.form.get("token", "")
|
||||
if token in _tokens:
|
||||
del _tokens[token]
|
||||
flash("Token revoked.", "success")
|
||||
|
||||
# Clean expired tokens
|
||||
now = datetime.now(UTC)
|
||||
expired = [t for t, d in _tokens.items() if datetime.fromisoformat(d["expires_at"]) < now]
|
||||
for t in expired:
|
||||
del _tokens[t]
|
||||
|
||||
return render_template("dropbox/admin.html", tokens=_tokens)
|
||||
|
||||
|
||||
def _validate_token(token: str) -> dict | None:
|
||||
"""Check if a token is valid. Returns token data or None."""
|
||||
if token not in _tokens:
|
||||
return None
|
||||
data = _tokens[token]
|
||||
if datetime.fromisoformat(data["expires_at"]) < datetime.now(UTC):
|
||||
del _tokens[token]
|
||||
return None
|
||||
if data["used"] >= data["max_files"]:
|
||||
return None
|
||||
return data
|
||||
|
||||
|
||||
@bp.route("/upload/<token>", methods=["GET", "POST"])
|
||||
def upload(token):
|
||||
"""Source-facing upload page. No authentication required."""
|
||||
token_data = _validate_token(token)
|
||||
if token_data is None:
|
||||
return Response(
|
||||
"This upload link has expired or is invalid.",
|
||||
status=404,
|
||||
content_type="text/plain",
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
files = request.files.getlist("files")
|
||||
if not files:
|
||||
return Response("No files provided.", status=400, content_type="text/plain")
|
||||
|
||||
_ensure_token_dir()
|
||||
receipts = []
|
||||
|
||||
for f in files:
|
||||
if token_data["used"] >= token_data["max_files"]:
|
||||
break
|
||||
|
||||
file_data = f.read()
|
||||
if not file_data:
|
||||
continue
|
||||
|
||||
# Strip EXIF metadata
|
||||
try:
|
||||
import io
|
||||
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(io.BytesIO(file_data))
|
||||
clean = io.BytesIO()
|
||||
img.save(clean, format=img.format or "PNG")
|
||||
file_data = clean.getvalue()
|
||||
except Exception:
|
||||
pass # Not an image, or Pillow can't handle it — keep as-is
|
||||
|
||||
# Compute SHA-256
|
||||
sha256 = hashlib.sha256(file_data).hexdigest()
|
||||
|
||||
# Save file
|
||||
dest = _TOKEN_DIR / f"{sha256[:16]}_{f.filename}"
|
||||
dest.write_bytes(file_data)
|
||||
|
||||
# Auto-attest
|
||||
chain_index = None
|
||||
try:
|
||||
from soosef.verisoo.attestation import create_attestation
|
||||
from soosef.verisoo.storage import LocalStorage
|
||||
|
||||
from blueprints.attest import _get_private_key, _get_storage
|
||||
|
||||
private_key = _get_private_key()
|
||||
if private_key:
|
||||
attestation = create_attestation(
|
||||
file_data, private_key, metadata={"source": "dropbox", "label": token_data["label"]}
|
||||
)
|
||||
storage = _get_storage()
|
||||
storage.append_record(attestation.record)
|
||||
except Exception:
|
||||
pass # Attestation is best-effort; don't fail the upload
|
||||
|
||||
# Generate receipt code
|
||||
receipt_code = secrets.token_hex(8)
|
||||
receipts.append({
|
||||
"filename": f.filename,
|
||||
"sha256": sha256,
|
||||
"receipt_code": receipt_code,
|
||||
"received_at": datetime.now(UTC).isoformat(),
|
||||
})
|
||||
|
||||
token_data["used"] += 1
|
||||
token_data["receipts"].append(receipt_code)
|
||||
|
||||
remaining = token_data["max_files"] - token_data["used"]
|
||||
|
||||
# Return receipt codes as plain text (minimal fingerprint)
|
||||
receipt_text = "FILES RECEIVED\n" + "=" * 40 + "\n\n"
|
||||
for r in receipts:
|
||||
receipt_text += f"File: {r['filename']}\n"
|
||||
receipt_text += f"Receipt: {r['receipt_code']}\n"
|
||||
receipt_text += f"SHA-256: {r['sha256']}\n\n"
|
||||
receipt_text += f"Remaining uploads on this link: {remaining}\n"
|
||||
receipt_text += "\nSave your receipt codes. They confirm your submission was received.\n"
|
||||
|
||||
return Response(receipt_text, content_type="text/plain")
|
||||
|
||||
# GET — show upload form (minimal, no SooSeF branding for source safety)
|
||||
remaining = token_data["max_files"] - token_data["used"]
|
||||
return f"""<!DOCTYPE html>
|
||||
<html><head><title>Secure Upload</title>
|
||||
<style>body{{font-family:sans-serif;max-width:600px;margin:40px auto;padding:20px}}
|
||||
input[type=file]{{margin:10px 0}}button{{padding:10px 20px}}</style></head>
|
||||
<body>
|
||||
<h2>Secure File Upload</h2>
|
||||
<p>Select files to upload. You may upload up to {remaining} file(s).</p>
|
||||
<p>Your files will be timestamped on receipt. No account or personal information is required.</p>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="file" name="files" multiple accept="image/*,.pdf,.doc,.docx,.txt"><br>
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
<p style="color:#666;font-size:12px">This link will expire automatically. Do not bookmark it.</p>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
@bp.route("/verify-receipt", methods=["POST"])
|
||||
def verify_receipt():
|
||||
"""Let a source verify their submission was received by receipt code."""
|
||||
code = request.form.get("code", "").strip()
|
||||
if not code:
|
||||
return Response("No receipt code provided.", status=400, content_type="text/plain")
|
||||
|
||||
for token_data in _tokens.values():
|
||||
if code in token_data["receipts"]:
|
||||
return Response(
|
||||
f"Receipt {code} is VALID. Your submission was received.",
|
||||
content_type="text/plain",
|
||||
)
|
||||
|
||||
return Response(
|
||||
f"Receipt {code} was not found. It may have expired.",
|
||||
status=404,
|
||||
content_type="text/plain",
|
||||
)
|
||||
Reference in New Issue
Block a user