Research-Stack/5-Applications/tools-scripts/publish/publish_evidence_package.py

418 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# ==============================================================================
# COPYRIGHT NO ONE EVERYWHERE LLC (WYOMING HOLDING COMPANY)
# PROJECT: SOVEREIGN STACK
# This artifact is entirely proprietary and cryptographically proven.
# Open-Source usage requires explicit permission from Brandon Scott Schneider.
# ==============================================================================
"""
publish_evidence_package.py
1. Bundle evidence artifacts into a zip.
2. Encrypt the zip with AES-256-GCM using a random 256-bit key.
3. Upload the encrypted blob to a public archive host (archive.org if
credentials are present, otherwise 0x0.st as the guerrilla fallback).
4. Send the trigger report email with the archive URL and decrypt key
directly to the destination MX server over port 25 (no relay, no auth).
Usage:
python 5-Applications/scripts/publish_evidence_package.py [--dry-run]
"""
import argparse
import hashlib
import io
import json
import os
import secrets
import smtplib
import socket
import struct
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from io_harness_compat import spawn_isolated_process
import zipfile
from datetime import datetime, timezone
from email.message import EmailMessage
from pathlib import Path
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "out"
SECRETS_DIR = ROOT / ".secrets" / "omnitoken_bridge"
PROFILE_PATH = ROOT / "out" / "omnitoken_bridge" / "proton_paranoid.json"
EVIDENCE_FILES = [
OUT / "defense_snapshot.json",
OUT / "defense_trigger_report.md",
OUT / "defense_handoff_brief.md",
OUT / "defense_loopback.zmlb",
OUT / "defense_loopback_payload.json",
OUT / "trace_attestation.json",
OUT / "trace_evidence_ledger.md",
OUT / "timeline.json",
OUT / "clusters.csv",
]
# ---------------------------------------------------------------------------
# Encryption (AES-256-GCM via stdlib struct + cryptography)
# ---------------------------------------------------------------------------
def _encrypt_aes256gcm(plaintext: bytes) -> tuple[bytes, str]:
"""Return (ciphertext_blob, hex_key).
Blob layout (all big-endian):
[4 bytes key_len] [32 bytes key (encrypted copy redundant but handy)]
[12 bytes nonce] [ciphertext + 16-byte tag]
The key is returned in hex so it can be transmitted separately.
"""
try:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = secrets.token_bytes(32)
nonce = secrets.token_bytes(12)
ct = AESGCM(key).encrypt(nonce, plaintext, None)
blob = struct.pack(">I", 32) + key + nonce + ct
return blob, key.hex()
except ImportError:
# Fallback: XOR with key stream (weak only if cryptography unavailable)
raise SystemExit(
"ERROR: 'cryptography' package is required. "
"Run: pip install cryptography"
)
def build_encrypted_bundle(paths: list[Path]) -> tuple[bytes, str, str]:
"""Zip evidence files, encrypt the zip, return (blob, hex_key, zip_sha256)."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for p in paths:
if p.exists():
zf.write(p, p.name)
else:
print(f" WARN: {p} not found, skipping", file=sys.stderr)
raw = buf.getvalue()
zip_sha = hashlib.sha256(raw).hexdigest()
blob, hex_key = _encrypt_aes256gcm(raw)
return blob, hex_key, zip_sha
# ---------------------------------------------------------------------------
# Upload
# ---------------------------------------------------------------------------
def _upload_0x0st(blob: bytes, filename: str) -> str:
"""Upload to https://0x0.st (no account, files persist ~1 year for ~100 KB)."""
import urllib.request, urllib.parse
boundary = secrets.token_hex(16)
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
f"Content-Type: application/octet-stream\r\n\r\n"
).encode() + blob + f"\r\n--{boundary}--\r\n".encode()
req = urllib.request.Request(
"https://0x0.st",
data=body,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"User-Agent": "publish-evidence/1.0",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=60) as resp:
return resp.read().decode().strip()
def _upload_archive_org(blob: bytes, identifier: str, filename: str) -> str:
"""Upload to archive.org using the internetarchive library."""
import internetarchive as ia
item = ia.get_item(identifier)
r = item.upload_file(
io.BytesIO(blob),
key=filename,
metadata={
"mediatype": "data",
"subject": "encrypted-evidence-package",
"description": "Encrypted forensic evidence bundle — key transmitted separately",
},
)
if not r.ok:
raise RuntimeError(f"archive.org upload failed: {r.status_code} {r.text}")
return f"https://archive.org/download/{identifier}/{filename}"
def upload_bundle(blob: bytes, ts: str) -> str:
"""Try archive.org first (if credentials exist), fall back to 0x0.st."""
filename = f"evidence-bundle-{ts}.bin"
# Check for archive.org credentials
ia_cfg = Path.home() / ".config" / "internetarchive" / "ia.ini"
alt_cfg = Path.home() / ".ia"
if ia_cfg.exists() or alt_cfg.exists():
identifier = f"evidence-bundle-{ts}"
try:
print(f" Uploading to archive.org as item: {identifier}")
url = _upload_archive_org(blob, identifier, filename)
print(f" archive.org URL: {url}")
return url
except Exception as exc:
print(f" archive.org failed ({exc}), falling back to 0x0.st")
print(f" Uploading {len(blob)/1024:.1f} KB to 0x0.st …")
url = _upload_0x0st(blob, filename)
print(f" 0x0.st URL: {url}")
return url
# ---------------------------------------------------------------------------
# Direct MX email delivery (port 25, no auth)
# ---------------------------------------------------------------------------
def _resolve_mx(domain: str) -> list[str]:
"""Return MX hostnames for the domain, sorted by preference."""
code, out, err = spawn_isolated_process(
["dig", "+short", "MX", domain],
timeout=10
)
hosts = []
for line in out.decode("utf-8").splitlines():
parts = line.strip().split()
if len(parts) == 2:
hosts.append((int(parts[0]), parts[1].rstrip(".")))
return [h for _, h in sorted(hosts)]
def send_via_relay(
host: str,
port: int,
user: str,
password: str,
to_addr: str,
from_addr: str,
subject: str,
body: str,
dry_run: bool = False,
) -> None:
"""Send via authenticated SMTP relay (port 587, STARTTLS)."""
msg = EmailMessage()
msg["From"] = from_addr
msg["To"] = to_addr
msg["Subject"] = subject
msg["Date"] = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
msg["Message-ID"] = f"<{secrets.token_hex(12)}@evidence-handoff.local>"
msg.set_content(body)
if dry_run:
print(f" [DRY-RUN] Would send via {host}:{port} as {user} to {to_addr}")
return
import ssl
ctx = ssl.create_default_context()
with smtplib.SMTP(host, port, timeout=20) as smtp:
smtp.ehlo()
smtp.starttls(context=ctx)
smtp.ehlo()
smtp.login(user, password)
smtp.sendmail(from_addr, [to_addr], msg.as_bytes())
print(f" Delivered via relay {host}:{port}")
def send_via_direct_mx(
to_addr: str,
from_addr: str,
subject: str,
body: str,
dry_run: bool = False,
) -> None:
"""Deliver directly to the destination MX over port 25 (unauthenticated)."""
domain = to_addr.split("@")[-1]
try:
mx_hosts = _resolve_mx(domain)
except Exception:
mx_hosts = []
if not mx_hosts:
# Fallback: try A record of the domain itself
mx_hosts = [domain]
print(f" MX hosts for {domain}: {mx_hosts}")
msg = EmailMessage()
msg["From"] = from_addr
msg["To"] = to_addr
msg["Subject"] = subject
msg["Date"] = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
msg["Message-ID"] = f"<{secrets.token_hex(12)}@evidence-handoff.local>"
msg.set_content(body)
last_err = None
for mx in mx_hosts:
if dry_run:
print(f" [DRY-RUN] Would connect to {mx}:25 and deliver to {to_addr}")
return
try:
print(f" Connecting to {mx}:25 …")
with smtplib.SMTP(mx, 25, timeout=20) as smtp:
smtp.ehlo("evidence-handoff.local")
# Try STARTTLS if offered
if smtp.has_extn("STARTTLS"):
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
smtp.starttls(context=ctx)
smtp.ehlo("evidence-handoff.local")
smtp.sendmail(from_addr, [to_addr], msg.as_bytes())
print(f" Delivered to {mx}:25")
return
except Exception as exc:
print(f" {mx}:25 failed: {exc}")
last_err = exc
raise RuntimeError(f"All MX hosts failed. Last error: {last_err}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--dry-run", action="store_true", help="Encrypt and upload but do not send the email")
ap.add_argument("--from-addr", default="2s3sa2.monthly496@passinbox.com", help="MAIL FROM address")
ap.add_argument("--to-addr", default="2s3sa2.monthly496@passinbox.com", help="Recipient")
ap.add_argument("--no-upload", action="store_true", help="Skip upload, only print what would be sent")
ap.add_argument("--smtp-host", default=None, help="SMTP relay host (e.g. smtp-relay.brevo.com)")
ap.add_argument("--smtp-port", type=int, default=587, help="SMTP relay port (default 587)")
ap.add_argument("--smtp-user", default=None, help="SMTP relay username/login")
ap.add_argument("--smtp-pass", default=None, help="SMTP relay password/API key")
args = ap.parse_args()
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
print(f"\n=== Evidence Package Publisher {ts} ===\n")
# 1. Build + encrypt bundle
print("[ 1 ] Building and encrypting evidence bundle …")
blob, hex_key, zip_sha = build_encrypted_bundle(EVIDENCE_FILES)
print(f" Zip SHA-256 : {zip_sha}")
print(f" Encrypted size : {len(blob)/1024:.1f} KB")
print(f" Decrypt key : {hex_key}")
# Save key + metadata locally just in case
key_record = {
"timestamp": ts,
"zip_sha256": zip_sha,
"decrypt_key_hex": hex_key,
"algorithm": "AES-256-GCM",
"note": "nonce is the first 12 bytes after the 4+32 byte header in the blob",
}
key_path = ROOT / "out" / "omnitoken_bridge" / f"bundle_key_{ts}.json"
key_path.write_text(json.dumps(key_record, indent=2))
os.chmod(key_path, 0o600)
print(f" Key record saved → {key_path}")
# 2. Upload
if args.no_upload:
archive_url = "[UPLOAD SKIPPED — --no-upload]"
else:
print("\n[ 2 ] Uploading encrypted bundle …")
archive_url = upload_bundle(blob, ts)
# 3. Compose email
print("\n[ 3 ] Composing notification email …")
trigger_report = (OUT / "defense_trigger_report.md").read_text(encoding="utf-8")
body = f"""\
Encrypted evidence bundle — automated handoff notification
===========================================================
Archive URL (AES-256-GCM encrypted):
{archive_url}
Decrypt key (AES-256-GCM, hex):
{hex_key}
Zip SHA-256 (pre-encryption plaintext):
{zip_sha}
Algorithm note:
The archive blob header layout is:
[4 bytes, big-endian] = 32 (key_len field, always 32)
[32 bytes] = the 256-bit AES key (redundant copy embedded in blob)
[12 bytes] = GCM nonce
[remaining bytes] = ciphertext + 16-byte GCM authentication tag
To decrypt with Python:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import struct, urllib.request
blob = urllib.request.urlopen('{archive_url}').read()
_, blob = struct.unpack('>I', blob[:4])[0], blob[4:]
key = bytes.fromhex('{hex_key}')
nonce, ct = blob[32:44], blob[44:]
plaintext_zip = AESGCM(key).decrypt(nonce, ct, None)
Or with openssl (extract nonce and ciphertext manually):
python3 5-Applications/scripts/publish_evidence_package.py --help (see decrypt helper)
---
{trigger_report}
---
This message was delivered directly to your MX via unauthenticated SMTP.
Timestamp: {ts}
"""
subject = f"[EVIDENCE HANDOFF] Encrypted bundle {ts} — decrypt key included"
print(f" To : {args.to_addr}")
print(f" From : {args.from_addr}")
print(f" Subject: {subject}")
# Save the email draft to disk regardless of send outcome
draft_path = ROOT / "out" / "omnitoken_bridge" / f"evidence_handoff_{ts}.eml"
draft_msg = EmailMessage()
draft_msg["From"] = args.from_addr
draft_msg["To"] = args.to_addr
draft_msg["Subject"] = subject
draft_msg["Date"] = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
draft_msg["Message-ID"] = f"<{ts}@evidence-handoff.local>"
draft_msg.set_content(body)
draft_path.write_bytes(draft_msg.as_bytes())
print(f" Draft saved → {draft_path}")
# 4. Send
print("\n[ 4 ] Delivering email via direct MX (port 25, no auth) …")
smtp_ok = False
try:
if args.smtp_host and args.smtp_user and args.smtp_pass:
print(f"\n[ 4 ] Delivering via SMTP relay {args.smtp_host}:{args.smtp_port}")
send_via_relay(
host=args.smtp_host,
port=args.smtp_port,
user=args.smtp_user,
password=args.smtp_pass,
to_addr=args.to_addr,
from_addr=args.from_addr,
subject=subject,
body=body,
dry_run=args.dry_run,
)
else:
print("\n[ 4 ] Delivering email via direct MX (port 25, no auth) …")
send_via_direct_mx(
to_addr=args.to_addr,
from_addr=args.from_addr,
subject=subject,
body=body,
dry_run=args.dry_run,
)
smtp_ok = True
except Exception as exc:
print(f"\n SMTP delivery failed: {exc}")
print(" The encrypted bundle is archived and the draft is saved locally.")
print(f" You can send {draft_path.name} manually via any mail client.")
print("\n=== Done ===")
print(f" Archive : {archive_url}")
print(f" Key : {hex_key}")
print(f" Draft EML : {draft_path}")
if args.dry_run:
print(" (dry-run — email was NOT actually sent)")
elif smtp_ok:
print(" SMTP: DELIVERED")
if __name__ == "__main__":
main()