scripts/crypto.py
scripts/crypto.pyBrowse 56 files
747 tokens
3,108 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1"""At-rest encryption for sensitive files via the `age` binary (optional).2 3Engaged ONLY when config `encryption: age` AND an age identity key exists AND the4`age`/`age-keygen` binaries are available. When engaged, JSON docs under5`subjects/` (dossier, ledger) are written as `<file>.age` ciphertext; the audit6log (field NAMES + states only, no raw PII values), `config.json`, and the broker7cache stay plaintext so the engine can read them.8 9Threat model (be honest): this protects against casual disk inspection, accidental10`git add`/commits, screen-shares, and backup/cloud-sync leakage. The identity key11defaults to living beside the data at `$PDD_DATA_DIR/age-identity.txt` (0600); set12`PDD_AGE_IDENTITY` to a separate volume/token for true key separation. It does NOT13protect against an attacker who can already read your whole HERMES_HOME (they get14key + data together).15"""16from __future__ import annotations17 18import json19import subprocess20from pathlib import Path21from shutil import which22 23import paths24 25 26def age_available() -> bool:27 return which("age") is not None and which("age-keygen") is not None28 29 30def encryption_setting() -> str:31 """Read `encryption` straight from config.json (no config/storage import => no cycle)."""32 cfg = paths.config_path()33 if not cfg.exists():34 return "none"35 try:36 return (json.loads(cfg.read_text(encoding="utf-8")) or {}).get("encryption", "none")37 except (ValueError, OSError):38 return "none"39 40 41def identity_path() -> Path:42 return paths.age_identity_path()43 44 45def ensure_identity() -> Path:46 """Generate an age identity (X25519 keypair) if missing; return its path."""47 if not age_available():48 raise RuntimeError("`age`/`age-keygen` not found; cannot enable encryption")49 p = identity_path()50 if not p.exists():51 p.parent.mkdir(parents=True, exist_ok=True)52 try:53 p.parent.chmod(0o700)54 except OSError:55 pass56 subprocess.run(["age-keygen", "-o", str(p)], check=True, capture_output=True)57 try:58 p.chmod(0o600)59 except OSError:60 pass61 return p62 63 64def recipient() -> str:65 """The age public key (recipient) for the identity, parsed from its header."""66 p = ensure_identity()67 for line in p.read_text(encoding="utf-8").splitlines():68 s = line.strip()69 if s.lower().startswith("# public key:"):70 return s.split(":", 1)[1].strip()71 if s.startswith("age1"):72 return s73 raise RuntimeError(f"no public key found in {p}")74 75 76def is_engaged() -> bool:77 """True only when encryption is actually active (configured + available + key present)."""78 return encryption_setting() == "age" and age_available() and identity_path().exists()79 80 81def encrypt(data: bytes) -> bytes:82 out = subprocess.run(["age", "-r", recipient()], input=data, capture_output=True, check=True)83 return out.stdout84 85 86def decrypt(data: bytes) -> bytes:87 out = subprocess.run(["age", "-d", "-i", str(identity_path())], input=data, capture_output=True, check=True)88 return out.stdout89