unbroker

Autonomously remove your info from data-broker sites.

  • privacy
  • data-broker
  • opt-out
  • ccpa
  • gdpr
  • security
  • doxxing

Declared platforms: linux · macos · windows

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/optional-skills/security/unbroker'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

scripts/crypto.py

scripts/crypto.pyBrowse 56 files
View on GitHub
← Back to SKILL.md
"""At-rest encryption for sensitive files via the `age` binary (optional). Engaged ONLY when config `encryption: age` AND an age identity key exists AND the`age`/`age-keygen` binaries are available. When engaged, JSON docs under`subjects/` (dossier, ledger) are written as `<file>.age` ciphertext; the auditlog (field NAMES + states only, no raw PII values), `config.json`, and the brokercache stay plaintext so the engine can read them. Threat model (be honest): this protects against casual disk inspection, accidental`git add`/commits, screen-shares, and backup/cloud-sync leakage. The identity keydefaults to living beside the data at `$PDD_DATA_DIR/age-identity.txt` (0600); set`PDD_AGE_IDENTITY` to a separate volume/token for true key separation. It does NOTprotect against an attacker who can already read your whole HERMES_HOME (they getkey + data together)."""from __future__ import annotations import jsonimport subprocessfrom pathlib import Pathfrom shutil import which import paths  def age_available() -> bool:    return which("age") is not None and which("age-keygen") is not None  def encryption_setting() -> str:    """Read `encryption` straight from config.json (no config/storage import => no cycle)."""    cfg = paths.config_path()    if not cfg.exists():        return "none"    try:        return (json.loads(cfg.read_text(encoding="utf-8")) or {}).get("encryption", "none")    except (ValueError, OSError):        return "none"  def identity_path() -> Path:    return paths.age_identity_path()  def ensure_identity() -> Path:    """Generate an age identity (X25519 keypair) if missing; return its path."""    if not age_available():        raise RuntimeError("`age`/`age-keygen` not found; cannot enable encryption")    p = identity_path()    if not p.exists():        p.parent.mkdir(parents=True, exist_ok=True)        try:            p.parent.chmod(0o700)        except OSError:            pass        subprocess.run(["age-keygen", "-o", str(p)], check=True, capture_output=True)        try:            p.chmod(0o600)        except OSError:            pass    return p  def recipient() -> str:    """The age public key (recipient) for the identity, parsed from its header."""    p = ensure_identity()    for line in p.read_text(encoding="utf-8").splitlines():        s = line.strip()        if s.lower().startswith("# public key:"):            return s.split(":", 1)[1].strip()        if s.startswith("age1"):            return s    raise RuntimeError(f"no public key found in {p}")  def is_engaged() -> bool:    """True only when encryption is actually active (configured + available + key present)."""    return encryption_setting() == "age" and age_available() and identity_path().exists()  def encrypt(data: bytes) -> bytes:    out = subprocess.run(["age", "-r", recipient()], input=data, capture_output=True, check=True)    return out.stdout  def decrypt(data: bytes) -> bytes:    out = subprocess.run(["age", "-d", "-i", str(identity_path())], input=data, capture_output=True, check=True)    return out.stdout