"""Stockage securise optionnel des secrets (mots de passe de session, cles API de l'assistant IA...). Par defaut, PySSH ne memorise aucun secret : ils sont redemandes a chaque usage. CredentialStore permet d'activer, au cas par cas, une memorisation via le trousseau du systeme - jamais en clair dans un fichier JSON, jamais logue. Sous Windows : Windows Credential Manager, via les fonctions CredWrite/ CredRead/CredDelete exposees par pywin32-ctypes (deja une dependance existante de l'application - inutile d'ajouter pywin32 ou keyring rien que pour ceci). Le secret est protege par DPAPI au niveau du systeme d'exploitation ; PySSH ne manipule jamais la cle de chiffrement elle-meme. Ailleurs (Linux/macOS) : aucun trousseau integre equivalent sans dependance supplementaire pour l'instant. NullCredentialStore signale simplement l'indisponibilite plutot que d'ecrire un secret en clair, pour ne jamais donner une fausse impression de securite. C'est le point d'extension prevu pour un futur backend (ex: Secret Service / Keychain). """ from __future__ import annotations import abc import os _TARGET_PREFIX = "PySSHClient/" def session_credential_key(session_id: str) -> str: return f"session:{session_id}" class CredentialStore(abc.ABC): """Une `key` est un identifiant applicatif (ex: "session:", "ai:api_key") : jamais le secret lui-meme.""" @abc.abstractmethod def is_available(self) -> bool: """False si aucun trousseau securise n'est utilisable sur ce poste.""" @abc.abstractmethod def set_secret(self, key: str, secret: str) -> bool: """Memorise `secret`. Renvoie False en cas d'echec (jamais d'exception).""" @abc.abstractmethod def get_secret(self, key: str) -> str | None: """None si absent ou si le trousseau est indisponible.""" @abc.abstractmethod def delete_secret(self, key: str) -> None: """Ne leve jamais si la cle est deja absente.""" class NullCredentialStore(CredentialStore): """Repli quand aucun trousseau securise n'est disponible.""" def is_available(self) -> bool: return False def set_secret(self, key: str, secret: str) -> bool: return False def get_secret(self, key: str) -> str | None: return None def delete_secret(self, key: str) -> None: pass class WindowsCredentialStore(CredentialStore): """Windows Credential Manager (CRED_TYPE_GENERIC).""" def __init__(self): try: import win32ctypes.pywin32.win32cred as win32cred self._win32cred = win32cred except ImportError: self._win32cred = None def is_available(self) -> bool: return self._win32cred is not None def _target(self, key: str) -> str: return _TARGET_PREFIX + key def set_secret(self, key: str, secret: str) -> bool: if self._win32cred is None: return False try: self._win32cred.CredWrite({ "Type": self._win32cred.CRED_TYPE_GENERIC, "TargetName": self._target(key), "UserName": os.environ.get("USERNAME", "pyssh"), "CredentialBlob": secret, "Comment": "PySSH Client", "Persist": self._win32cred.CRED_PERSIST_LOCAL_MACHINE, }, 0) return True except Exception: # noqa: BLE001 - jamais bloquant : au pire, on redemandera le secret return False def get_secret(self, key: str) -> str | None: if self._win32cred is None: return None try: cred = self._win32cred.CredRead(self._target(key), self._win32cred.CRED_TYPE_GENERIC, 0) except Exception: # noqa: BLE001 - absent ou illisible : redemander est le repli correct return None blob = cred.get("CredentialBlob") or b"" try: return blob.decode("utf-16-le") except UnicodeDecodeError: return None def delete_secret(self, key: str) -> None: if self._win32cred is None: return try: self._win32cred.CredDelete(self._target(key), self._win32cred.CRED_TYPE_GENERIC, 0) except Exception: # noqa: BLE001 - deja absent, ou trousseau indisponible : rien a faire pass def default_credential_store() -> CredentialStore: if os.name == "nt": store = WindowsCredentialStore() if store.is_available(): return store return NullCredentialStore()