#!/usr/bin/env python3 """Credential storage for unattended recording. Secrets live in the Windows Credential Manager (DPAPI, keyed to your Windows account), not in a file. Nothing here can be committed by accident because nothing here is written to the repo. python creds.py --set-login store email + password (prompts) python creds.py --show show what is stored (never the password) python creds.py --clear remove everything What this does and does not protect against: DOES -- plaintext at rest, and the classic "gitignored file gets committed after a git add -A" accident. DOES -- readable by another Windows user on this machine. NOT -- a process already running as you. Credential Manager unlocks with your login session, so anything running as you can read it. That is inherent to unattended operation: a job that logs in without you present must be able to reach the secret without you present. Caveat for Task Scheduler: "Run whether user is logged on or not" runs in a session where DPAPI may not be able to decrypt these entries. Prefer "Run only when user is logged on" for any task that needs credentials. PLATFORM NOTE -- this module is only safe where a real vault backend exists: Windows WinVaultKeyring (Credential Manager, DPAPI). Good. Linux+GUI SecretService (GNOME Keyring / KWallet). Good. Headless No D-Bus session, no unlocked keyring. `keyring` NOT SAFE. Linux falls back to a fail or plaintext backend. On a headless Pi or server, do NOT use this module. Use systemd-creds, which encrypts to the host/TPM key and is decrypted only for the service: systemd-creds encrypt --name=brfc-pass - /etc/credstore.encrypted/brfc-pass # then in the unit: LoadCredential=brfc-pass # and read it from: $CREDENTIALS_DIRECTORY/brfc-pass Run `python creds.py --check` on each host before trusting it with a secret. """ import argparse import getpass import sys try: import keyring except ImportError: sys.exit("keyring not installed. Run: pip install keyring") SERVICE = "brfc-stream-pirate" # Keys held in the vault. K_USER = "username" K_PASS = "password" K_COOKIE = "session-cookie" # cached login session, refreshed periodically def get(key): return keyring.get_password(SERVICE, key) def put(key, value): keyring.set_password(SERVICE, key, value) def drop(key): try: keyring.delete_password(SERVICE, key) except keyring.errors.PasswordDeleteError: pass # already absent def load_login(): """Return (username, password), or None if not configured yet.""" user, pw = get(K_USER), get(K_PASS) return (user, pw) if user and pw else None def load_cookie(): """Cached session cookie, or None. Callers must handle it being stale.""" return get(K_COOKIE) def save_cookie(value): put(K_COOKIE, value) # Backends that do not actually protect anything. `keyring` selects these # silently on a headless box, so a secret stored there is effectively plaintext. UNSAFE_BACKENDS = {"PlaintextKeyring", "Keyring", "fail.Keyring", "EnvironCredential"} def backend(): return keyring.get_keyring().__class__.__name__ def backend_is_safe(): name = backend() return name not in UNSAFE_BACKENDS and "fail" not in name.lower() def _mask(s): if not s: return "(not set)" return s[:2] + "*" * max(0, len(s) - 4) + s[-2:] if len(s) > 4 else "*" * len(s) def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) g = ap.add_mutually_exclusive_group(required=True) g.add_argument("--set-login", action="store_true", help="store email + password") g.add_argument("--show", action="store_true", help="show stored username") g.add_argument("--clear", action="store_true", help="remove all stored secrets") g.add_argument("--check", action="store_true", help="report whether this host has a real vault backend") args = ap.parse_args() if args.check: name = backend() safe = backend_is_safe() print(f"backend : {name}") print(f"verdict : {'SAFE' if safe else 'NOT SAFE -- do not store secrets here'}") if not safe: print(" use systemd-creds on this host (see module docstring)") sys.exit(0 if safe else 1) if not backend_is_safe(): sys.exit(f"refusing to use backend {backend()!r}: it does not encrypt. " "Run 'python creds.py --check' for guidance.") if args.set_login: user = input("email: ").strip() if not user: sys.exit("aborted: empty email") pw = getpass.getpass("password: ") if not pw: sys.exit("aborted: empty password") put(K_USER, user) put(K_PASS, pw) print(f"stored in Windows Credential Manager under '{SERVICE}'") elif args.show: print(f"service : {SERVICE}") print(f"username: {get(K_USER) or '(not set)'}") print(f"password: {'(set)' if get(K_PASS) else '(not set)'}") print(f"cookie : {_mask(get(K_COOKIE))}") elif args.clear: for k in (K_USER, K_PASS, K_COOKIE): drop(k) print("cleared") if __name__ == "__main__": main()