#!/usr/bin/env python3 """BSN Secrets Manager — GPG-verschlüsselte Secrets für alle BSN-Agenten. Usage: bsn-secrets set # Wert von stdin einlesen und verschlüsselt speichern bsn-secrets get # Entschlüsselten Wert auf stdout ausgeben bsn-secrets list # Alle gespeicherten Secrets auflisten bsn-secrets delete # Secret löschen Konfiguration: Passphrase aus BSN_SECRETS_PASSPHRASE (aus Hermes .env) Store-Verzeichnis: ~/.password-store/bsn/ Beispiele: echo "abc123token" | bsn-secrets set gitea-token bsn-secrets get gitea-token TOKEN=$(bsn-secrets get gitea-token) """ import os import sys import subprocess import json from pathlib import Path STORE = Path.home() / ".password-store" / "bsn" def _get_passphrase() -> str: """Passphrase aus Umgebungsvariable oder .env-Dateien lesen.""" pp = os.getenv("BSN_SECRETS_PASSPHRASE", "") if pp: return pp # Fallback: Aus Hermes .env lesen for env_path in [ Path.home() / ".hermes" / "profiles" / "cody" / ".env", Path.home() / ".hermes" / ".env", ]: if env_path.exists(): for line in env_path.read_text().split("\n"): if line.startswith("BSN_SECRETS_PASSPHRASE="): return line.split("=", 1)[1].strip().strip('"').strip("'") return "" def _encrypt(plaintext: str, passphrase: str) -> bytes: """GPG-symmetrisch verschlüsseln.""" result = subprocess.run( [ "gpg", "--symmetric", "--batch", "--yes", "--passphrase", passphrase, "--quiet", "--no-tty", ], input=plaintext.encode("utf-8"), capture_output=True, ) if result.returncode != 0: print(f"❌ GPG-Verschlüsselung fehlgeschlagen: {result.stderr.decode()}", file=sys.stderr) sys.exit(1) return result.stdout def _decrypt(filepath: Path, passphrase: str) -> str: """GPG-symmetrisch entschlüsseln.""" result = subprocess.run( [ "gpg", "--decrypt", "--batch", "--yes", "--passphrase", passphrase, "--quiet", "--no-tty", str(filepath), ], capture_output=True, ) if result.returncode != 0: print(f"❌ GPG-Entschlüsselung fehlgeschlagen: {result.stderr.decode()}", file=sys.stderr) sys.exit(1) return result.stdout.decode("utf-8").strip() def _resolve_path(name: str) -> Path: """Namen in Dateipfad auflösen. 'foo/bar' → STORE/foo/bar.gpg, 'bar' → STORE/bar.gpg.""" parts = name.split("/") parent = STORE.joinpath(*parts[:-1]) if len(parts) > 1 else STORE parent.mkdir(parents=True, exist_ok=True) return parent / f"{parts[-1]}.gpg" def cmd_set(name: str) -> None: """Secret von stdin lesen und verschlüsselt speichern.""" passphrase = _get_passphrase() if not passphrase: print("❌ BSN_SECRETS_PASSPHRASE nicht gesetzt!", file=sys.stderr) sys.exit(1) plaintext = sys.stdin.read().strip() if not plaintext: print("❌ Kein Input auf stdin!", file=sys.stderr) sys.exit(1) encrypted = _encrypt(plaintext, passphrase) filepath = _resolve_path(name) filepath.write_bytes(encrypted) filepath.chmod(0o600) print(f"✅ Secret '{name}' gespeichert ({len(plaintext)} Zeichen)") def cmd_get(name: str) -> None: """Secret entschlüsseln und auf stdout ausgeben.""" passphrase = _get_passphrase() if not passphrase: print("❌ BSN_SECRETS_PASSPHRASE nicht gesetzt!", file=sys.stderr) sys.exit(1) filepath = _resolve_path(name) if not filepath.exists(): print(f"❌ Secret '{name}' nicht gefunden!", file=sys.stderr) sys.exit(1) plaintext = _decrypt(filepath, passphrase) sys.stdout.write(plaintext) def cmd_list() -> None: """Alle gespeicherten Secrets auflisten.""" if not STORE.exists(): print("(keine Secrets gespeichert)") return secrets = sorted(STORE.rglob("*.gpg")) if not secrets: print("(keine Secrets gespeichert)") return for f in secrets: rel = f.relative_to(STORE) name = str(rel.with_suffix("")) # Ohne .gpg size = f.stat().st_size print(f" 🔒 {name:30s} ({size} bytes)") def cmd_delete(name: str) -> None: """Secret löschen.""" filepath = _resolve_path(name) if not filepath.exists(): print(f"❌ Secret '{name}' nicht gefunden!", file=sys.stderr) sys.exit(1) filepath.unlink() print(f"🗑️ Secret '{name}' gelöscht") def main(): if len(sys.argv) < 2: print(__doc__) sys.exit(0) cmd = sys.argv[1] if cmd == "set": if len(sys.argv) < 3: print("Usage: bsn-secrets set ", file=sys.stderr) sys.exit(1) cmd_set(sys.argv[2]) elif cmd == "get": if len(sys.argv) < 3: print("Usage: bsn-secrets get ", file=sys.stderr) sys.exit(1) cmd_get(sys.argv[2]) elif cmd == "list": cmd_list() elif cmd == "delete": if len(sys.argv) < 3: print("Usage: bsn-secrets delete ", file=sys.stderr) sys.exit(1) cmd_delete(sys.argv[2]) else: print(f"❌ Unbekannter Befehl: {cmd}", file=sys.stderr) print(__doc__) sys.exit(1) if __name__ == "__main__": main()