gabriel / muse public
keychain.py python
143 lines 4.0 KB
Raw
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