mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
418 lines
15 KiB
Python
418 lines
15 KiB
Python
#!/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()
|