keychain.py
python
sha256:2fa778aba8ab0ec15295b8624c6480a573482ffc9c206a6d9546f1c41d2c2b7b
feat: supercharge muse blame + remove --porcelain everywhere
Human
patch
164 days ago
| 1 | """Secure credential storage via OS keychain. |
| 2 | |
| 3 | BIP39 mnemonics are stored here — never in plaintext TOML files. |
| 4 | |
| 5 | The module wraps the ``keyring`` library, which selects the best available |
| 6 | backend automatically: |
| 7 | |
| 8 | macOS → Keychain Services (Secure Enclave on Apple Silicon) |
| 9 | Linux → SecretService (GNOME Keyring / KWallet) |
| 10 | Headless → ``keyrings.alt`` encrypted file (AES + PBKDF2) |
| 11 | |
| 12 | Test / CI isolation |
| 13 | ------------------- |
| 14 | Set ``MUSE_KEYCHAIN_BACKEND=disabled`` to bypass the keychain entirely. |
| 15 | Under this setting all operations are no-ops and :func:`is_available` |
| 16 | returns ``False``. Callers must handle mnemonic absence gracefully (the |
| 17 | mnemonic is ephemeral for the lifetime of the process). |
| 18 | |
| 19 | Key format |
| 20 | ---------- |
| 21 | :: |
| 22 | |
| 23 | service = "muse" |
| 24 | username = "<hostname>/mnemonic" e.g. "localhost:10003/mnemonic" |
| 25 | |
| 26 | Public API |
| 27 | ---------- |
| 28 | :: |
| 29 | |
| 30 | is_available() -> bool |
| 31 | store(hub_url, mnemonic) -> bool |
| 32 | load(hub_url) -> str | None |
| 33 | delete(hub_url) -> bool |
| 34 | """ |
| 35 | |
| 36 | from __future__ import annotations |
| 37 | |
| 38 | import logging |
| 39 | import os |
| 40 | |
| 41 | logger = logging.getLogger(__name__) |
| 42 | |
| 43 | _SERVICE = "muse" |
| 44 | |
| 45 | |
| 46 | def _username(hub_url: str) -> str: |
| 47 | """Return the keychain key for the mnemonic of *hub_url*. |
| 48 | |
| 49 | Args: |
| 50 | hub_url: Hub URL or bare hostname (e.g. ``"http://localhost:10003"``). |
| 51 | |
| 52 | Returns: |
| 53 | Keychain username string of the form ``"<hostname>/mnemonic"``. |
| 54 | """ |
| 55 | from muse.core.identity import hostname_from_url |
| 56 | return f"{hostname_from_url(hub_url)}/mnemonic" |
| 57 | |
| 58 | |
| 59 | def is_available() -> bool: |
| 60 | """Return ``True`` when a functional keychain backend is active. |
| 61 | |
| 62 | Returns ``False`` when: |
| 63 | - ``MUSE_KEYCHAIN_BACKEND=disabled`` is set (test/CI mode), or |
| 64 | - no keychain backend with priority > 0 is found, or |
| 65 | - the ``keyring`` library is not installed. |
| 66 | |
| 67 | Returns: |
| 68 | ``True`` if credentials can be stored and retrieved. |
| 69 | """ |
| 70 | if os.environ.get("MUSE_KEYCHAIN_BACKEND") == "disabled": |
| 71 | return False |
| 72 | try: |
| 73 | import keyring |
| 74 | backend = keyring.get_keyring() |
| 75 | return getattr(backend, "priority", 0) > 0 |
| 76 | except Exception: |
| 77 | return False |
| 78 | |
| 79 | |
| 80 | def store(hub_url: str, mnemonic: str) -> bool: |
| 81 | """Store *mnemonic* in the OS keychain for *hub_url*. |
| 82 | |
| 83 | Args: |
| 84 | hub_url: Hub URL or bare hostname. |
| 85 | mnemonic: BIP39 mnemonic phrase to store. |
| 86 | |
| 87 | Returns: |
| 88 | ``True`` on success, ``False`` if the keychain is unavailable or |
| 89 | the store operation fails. |
| 90 | """ |
| 91 | if not is_available(): |
| 92 | return False |
| 93 | try: |
| 94 | import keyring |
| 95 | keyring.set_password(_SERVICE, _username(hub_url), mnemonic) |
| 96 | logger.debug("✅ Mnemonic stored in keychain for %s", hub_url) |
| 97 | return True |
| 98 | except Exception as exc: |
| 99 | logger.warning("⚠️ Could not store mnemonic in keychain: %s", exc) |
| 100 | return False |
| 101 | |
| 102 | |
| 103 | def load(hub_url: str) -> str | None: |
| 104 | """Load the mnemonic for *hub_url* from the OS keychain. |
| 105 | |
| 106 | Args: |
| 107 | hub_url: Hub URL or bare hostname. |
| 108 | |
| 109 | Returns: |
| 110 | The mnemonic phrase, or ``None`` if not found or keychain unavailable. |
| 111 | """ |
| 112 | if not is_available(): |
| 113 | return None |
| 114 | try: |
| 115 | import keyring |
| 116 | return keyring.get_password(_SERVICE, _username(hub_url)) |
| 117 | except Exception as exc: |
| 118 | logger.warning("⚠️ Could not load mnemonic from keychain: %s", exc) |
| 119 | return None |
| 120 | |
| 121 | |
| 122 | def delete(hub_url: str) -> bool: |
| 123 | """Delete the mnemonic for *hub_url* from the OS keychain. |
| 124 | |
| 125 | Args: |
| 126 | hub_url: Hub URL or bare hostname. |
| 127 | |
| 128 | Returns: |
| 129 | ``True`` if the entry was deleted, ``False`` if it was not found or |
| 130 | the keychain is unavailable. |
| 131 | """ |
| 132 | if not is_available(): |
| 133 | return False |
| 134 | try: |
| 135 | import keyring |
| 136 | import keyring.errors |
| 137 | keyring.delete_password(_SERVICE, _username(hub_url)) |
| 138 | logger.debug("✅ Mnemonic deleted from keychain for %s", hub_url) |
| 139 | return True |
| 140 | except Exception as exc: |
| 141 | # PasswordDeleteError means the entry didn't exist — not an error. |
| 142 | logger.debug("keychain delete: %s", exc) |
| 143 | return False |
File History
1 commit
sha256:2fa778aba8ab0ec15295b8624c6480a573482ffc9c206a6d9546f1c41d2c2b7b
feat: supercharge muse blame + remove --porcelain everywhere
Human
patch
164 days ago