#! /usr/bin/env python3 """Evan's Secrets tool This aims to wrap the different secret management tools used on systems I work with. For now, this only targets `secret-tool`, but I plan to add more in the future. """ import argparse import sys import logging import shutil import subprocess import sqlite3 from pathlib import Path from typing import Optional from abc import ABC, abstractmethod logger = logging.getLogger(__name__) __all__ = ["EwpSecrets"] class __SecretManager(ABC): @abstractmethod def runs_on_this_system(self) -> bool: ... @abstractmethod def store(self, namespace: str, key: str, secret: str): ... @abstractmethod def load(self, namespace: str, key: str) -> Optional[str]: ... def is_secure(self) -> bool: return True class GnomeKeyringSM(__SecretManager): def runs_on_this_system(self) -> bool: return shutil.which("secret-tool") is not None def store(self, namespace: str, key: str, secret: str): process = subprocess.Popen( [ "secret-tool", "store", "--label", "Secret stored by ewp-secrets", namespace, key, ], stdin=subprocess.PIPE, ) process.communicate(input=secret.encode()) process.wait() def load(self, namespace: str, key: str) -> Optional[str]: try: process = subprocess.run( ["secret-tool", "lookup", namespace, key], check=True, capture_output=True, ) return process.stdout.decode() except subprocess.CalledProcessError: return None class FilesystemSM(__SecretManager): def __init__( self, storage_path: Path = Path("~/.config/ewp-secrets/storage.sqlite3").expanduser(), ): # If the file doesn't exist, create it and restrict access if not storage_path.exists(): storage_path.parent.mkdir(parents=True, exist_ok=True) storage_path.touch() storage_path.chmod(0o600) self.conn = sqlite3.connect(storage_path) self.conn.execute( """CREATE TABLE IF NOT EXISTS secrets ( namespace TEXT, key TEXT, secret TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (namespace, key) ) """ ) def runs_on_this_system(self) -> bool: return True def store(self, namespace: str, key: str, secret: str): self.conn.execute( """INSERT INTO secrets (namespace, key, secret) VALUES (?, ?, ?) ON CONFLICT (namespace, key) DO UPDATE SET (secret, updated_at) = (?, CURRENT_TIMESTAMP) """, (namespace, key, secret, secret), ) def load(self, namespace: str, key: str) -> Optional[str]: cursor = self.conn.execute( """SELECT secret FROM secrets WHERE namespace = ? AND key = ?""", (namespace, key), ) result = cursor.fetchone() if result: self.conn.execute( """UPDATE secrets SET accessed_at = CURRENT_TIMESTAMP WHERE namespace = ? AND key = ?""", (namespace, key), ) return result[0] return None def is_secure(self) -> bool: return False class EwpSecrets: def __init__(self): all_secret_managers = [GnomeKeyringSM(), FilesystemSM()] self.secret_managers = [ sm for sm in all_secret_managers if sm.runs_on_this_system() ] assert self.secret_managers, "No secret managers available on this system" def store(self, namespace: str, key: str, secret: str): # Only write to the first (best) secret manager self.secret_managers[0].store(namespace, key, secret) def load(self, namespace: str, key: str) -> Optional[str]: # Try to read from each secret manager until we find the secret for sm in self.secret_managers: secret = sm.load(namespace, key) if secret: return secret return None def main() -> int: # Handle program arguments ap = argparse.ArgumentParser( prog="ewp-secrets", description="Store and load secrets" ) ap.add_argument("action", help="Action to perform", choices=["store", "load"]) ap.add_argument( "-n", "--namespace", help="Namespace to store secrets in", required=True ) ap.add_argument("-k", "--key", help="Key to store secret under" , required=True) ap.add_argument( "-v", "--verbose", help="Enable verbose logging", action="store_true" ) args = ap.parse_args() # Configure logging logging.basicConfig( level=logging.DEBUG if args.verbose else logging.INFO, format="%(levelname)s: %(message)s", ) # Access the secret manager secrets = EwpSecrets() # Perform the requested action if args.action == "store": if not secrets.secret_managers[0].is_secure(): print("Warning: This system does not have a secure way to store secrets", file=sys.stderr) secret = input("Enter the secret: ") secrets.store(args.namespace, args.key, secret) return 0 elif args.action == "load": secret = secrets.load(args.namespace, args.key) if secret: print(secret) return 0 else: print("No secret found", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())