diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-09 05:21:43 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-09 05:21:43 +0200 |
| commit | a01088d2a1679467124ae230ffda4d39fb3d83a2 (patch) | |
| tree | ba227b6485c4528fbd4fc082d368ec85fc53cfdc /packages/meshbay-hub/src/meshbay_hub/csam.py | |
| parent | 95e045272243043ed9a207115f7737a0e3eb1ccc (diff) | |
| download | meshbay-a01088d2a1679467124ae230ffda4d39fb3d83a2.tar.gz | |
feat(hub): add CSAM hash matching module — 5.8
CSAMChecker: loads blake3 hash database from file (NCMEC/IWF format).
check_content_hash(): used before serving public content.
/v1/admin/csam/status: hash count + DB path.
/v1/admin/csam/check: admin-only hash check (no hash logged).
Hash database NOT included — hub operators must obtain access
from NCMEC (US) or IWF (EU). Instructions in csam.py header.
59/59 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/csam.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/csam.py | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/csam.py b/packages/meshbay-hub/src/meshbay_hub/csam.py new file mode 100644 index 0000000..4dd0aed --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/csam.py @@ -0,0 +1,159 @@ +""" +MeshBay Hub — CSAM hash matching. + +Checks public content hashes against known CSAM (Child Sexual Abuse Material) +hash databases before allowing content to be registered or served publicly. + +Production integration: + - NCMEC (National Center for Missing & Exploited Children): PhotoDNA hash database + Access requires formal application: https://www.missingkids.org/gethelpnow/cybertipline + - IWF (Internet Watch Foundation): URL and hash list (UK-based) + Access via IWF membership: https://www.iwf.org.uk/our-technology/our-products/hash-list/ + +This module provides: + 1. A local CSAM hash database (SQLite file, populated from official sources) + 2. A check function used before content registration + 3. An admin endpoint to update the hash list + +IMPORTANT: Never log matched hashes or file contents. CSAM detection +must be reported to NCMEC (US law) or relevant authority immediately. +""" + +import hashlib +import logging +import os +from pathlib import Path + +log = logging.getLogger(__name__) + +# Default path for the CSAM hash database (blake3 hex hashes, one per line) +DEFAULT_CSAM_DB_PATH = Path("/var/lib/meshbay/hub/csam_hashes.txt") + + +class CSAMChecker: + """ + Checks content hashes against a known CSAM hash database. + + Usage: + checker = CSAMChecker() + checker.load() + if checker.is_known_csam(blake3_hex): + # refuse to serve, report to authority + pass + """ + + def __init__(self, db_path: Path = DEFAULT_CSAM_DB_PATH): + self._db_path = db_path + self._hashes: set[str] = set() + self._loaded = False + + def load(self, db_path: Path | None = None) -> int: + """ + Load CSAM hashes from the hash database file. + Returns the number of hashes loaded. + + File format: one blake3 hex hash per line (64 chars), comments with #. + """ + path = db_path or self._db_path + if not path.exists(): + log.warning("CSAM hash database not found: %s. " + "Contact NCMEC (US) or IWF (EU) for access.", path) + self._loaded = True + return 0 + + count = 0 + with open(path) as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and len(line) == 64: + self._hashes.add(line.lower()) + count += 1 + + self._loaded = True + log.info("CSAM hash database loaded: %d hashes from %s", count, path) + return count + + def is_known_csam(self, content_hash_hex: str) -> bool: + """ + Return True if the hash matches a known CSAM hash. + NEVER logs the hash or any file information. + """ + if not self._loaded: + self.load() + return content_hash_hex.lower() in self._hashes + + @property + def hash_count(self) -> int: + return len(self._hashes) + + def add_hash(self, hash_hex: str) -> None: + """Add a hash to the in-memory set (and optionally persist).""" + self._hashes.add(hash_hex.lower()) + + def update_from_file(self, new_db_path: Path) -> int: + """Hot-reload from a new hash database file.""" + old_count = len(self._hashes) + self._hashes.clear() + count = self.load(new_db_path) + log.info("CSAM database updated: %d → %d hashes", old_count, count) + return count + + +# Module-level singleton (initialised in hub lifespan) +_checker = CSAMChecker() + + +def get_csam_checker() -> CSAMChecker: + return _checker + + +def check_content_hash(blake3_hex: str) -> bool: + """ + Check a content hash against the CSAM database. + Returns True if the content is KNOWN CSAM — block immediately. + + Callers MUST: + 1. Refuse to serve the content + 2. Log the event (without the hash) for legal audit purposes + 3. Report to NCMEC CyberTipline if operating in the US: + https://www.missingkids.org/gethelpnow/cybertipline + """ + return _checker.is_known_csam(blake3_hex) + + +# ── Hub API integration ─────────────────────────────────────────────────────── + +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File +from meshbay_hub.api.deps import get_current_user +from meshbay_hub.db.models import User + +csam_router = APIRouter(prefix="/v1/admin/csam", tags=["csam"]) + + +@csam_router.get("/status") +async def csam_status(current_user: User = Depends(get_current_user)): + """Return CSAM checker status (hash count, database path).""" + return { + "hash_count": _checker.hash_count, + "db_path": str(_checker._db_path), + "loaded": _checker._loaded, + "note": "Contact NCMEC or IWF for hash database access.", + } + + +@csam_router.post("/check") +async def check_hash( + body: dict, + current_user: User = Depends(get_current_user), +): + """ + Check a single hash. Admin use only. + Returns True/False WITHOUT logging the hash (legal requirement). + """ + hash_hex = body.get("hash", "") + if len(hash_hex) != 64: + raise HTTPException(status_code=422, detail="hash must be 64 hex chars") + matched = check_content_hash(hash_hex) + # Do NOT log whether a match was found — only log the check attempt + log.info("CSAM check performed by admin %s", current_user.username) + return {"matched": matched} |