fix(hub): Hub login must accept the password the desktop accepts
verify_system_password() trusted the first loadable libcrypt. A library that cannot speak the shadow hash's algorithm (yescrypt, NixOS's default) answers with the '*0' sentinel, which was compared to the hash and reported as a wrong password -- even though the same password still signed in at the desktop. Following the System Passwords tile's 'sign out and back in' hint then locked the user out of the Hub, asking for a 'new' Hub password that does not exist: Hub login is the free account password, verified against /etc/shadow. Try every loadable libcrypt, treat None/'*' replies as inconclusive rather than wrong, and when no library can decide, verify via PAM -- the same check GDM performs. Also replaces the sign-out hint (server + JS fallback) with wording that says the Hub has no separate password and not to sign out unless you know the current desktop password. Recovery for an already locked-out user whose desktop login still works: on the machine itself, clear the localhost:8937 cookies and open http://localhost:8937/auto-login (localhost-only), or run 'sudo change-free-password' in a terminal to set a known free/Hub password.
This commit is contained in:
1 parent
e718404e84
commit
d84e0c4d82
4 files changed
+194
-29
No files matched your search
@@ -87,6 +87,59 @@ class VerifyNeverRaises(unittest.TestCase):
|
||||
self.assertIsNone(self.sp.read_root_display_password())
|
||||
|
||||
|
||||
class WrongLibraryMustNotLockTheUserOut(unittest.TestCase):
|
||||
"""A loadable libcrypt that cannot speak the shadow hash's algorithm
|
||||
(yescrypt $y$ on NixOS) answers with the "*0" sentinel. That used to be
|
||||
compared against the hash, found unequal, and reported as a *wrong
|
||||
password* at Hub login -- locking out a user whose desktop login with
|
||||
the very same password still worked, after the tile's hint told them
|
||||
to sign out. Inconclusive answers must fall through to the next
|
||||
library, and finally to PAM (the check GDM itself performs)."""
|
||||
|
||||
def setUp(self):
|
||||
self.sp = _load_system_password()
|
||||
self.sp.shadow_hash = lambda user="free": _KNOWN_HASH
|
||||
self._old_libs = self.sp._libcrypt_libs
|
||||
|
||||
def tearDown(self):
|
||||
self.sp._libcrypt_libs = self._old_libs
|
||||
|
||||
def test_sentinel_from_first_library_falls_through_to_the_next(self):
|
||||
class _SentinelLib:
|
||||
@staticmethod
|
||||
def crypt(_pw, _salt):
|
||||
return b"*0"
|
||||
|
||||
class _RealLib:
|
||||
@staticmethod
|
||||
def crypt(pw, salt):
|
||||
return _KNOWN_HASH.encode() if pw == b"hello" else b"$6$saltsalt$nomatch"
|
||||
|
||||
self.sp._libcrypt_libs = [_SentinelLib, _RealLib]
|
||||
self.assertTrue(self.sp.verify_system_password("hello"))
|
||||
self.assertFalse(self.sp.verify_system_password("not-the-password"))
|
||||
|
||||
def test_when_no_library_understands_the_hash_pam_decides(self):
|
||||
class _SentinelLib:
|
||||
@staticmethod
|
||||
def crypt(_pw, _salt):
|
||||
return b"*0"
|
||||
|
||||
self.sp._libcrypt_libs = [_SentinelLib]
|
||||
with unittest.mock.patch.object(
|
||||
self.sp, "_verify_via_pam", return_value=True
|
||||
) as pam:
|
||||
self.assertTrue(self.sp.verify_system_password("hello"))
|
||||
pam.assert_called_once()
|
||||
|
||||
def test_hint_no_longer_tells_the_user_to_sign_out_blind(self):
|
||||
for text in (
|
||||
_read("app", "sovran_systemsos_web", "server.py"),
|
||||
_read("app", "sovran_systemsos_web", "static", "js", "service-detail.js"),
|
||||
):
|
||||
self.assertNotIn("sign out and back in to show it here", text)
|
||||
|
||||
|
||||
class ServiceDetailCannotBeKilledByOneCredential(unittest.TestCase):
|
||||
"""Source guards (server.py is not importable without the full Hub deps,
|
||||
same reason the other suites read it as text)."""
|
||||
|
||||
Reference in new issue
Block a user