From cffb2aaae30e0a470b4565c3f838f3f375c4d562 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 9 Mar 2025 08:49:30 +0100 Subject: [PATCH 1/4] add support for additional bcrypt algo on autodetect, improve autodetect logic and log not matching hash length --- radicale/auth/htpasswd.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/radicale/auth/htpasswd.py b/radicale/auth/htpasswd.py index ed593301..bdec74c1 100644 --- a/radicale/auth/htpasswd.py +++ b/radicale/auth/htpasswd.py @@ -51,6 +51,7 @@ When bcrypt is installed: import functools import hmac import os +import re import threading import time from typing import Any, Tuple @@ -131,33 +132,49 @@ class Auth(auth.BaseAuth): """Check if ``hash_value`` and ``password`` match, plain method.""" return ("PLAIN", hmac.compare_digest(hash_value.encode(), password.encode())) + def _plain_fallback(self, method_orig, hash_value: str, password: str) -> tuple[str, bool]: + """Check if ``hash_value`` and ``password`` match, plain method / fallback in case of hash length is not matching on autodetection.""" + info = "PLAIN/fallback as hash length not matching for " + method_orig + ": " + str(len(hash_value)) + return (info, hmac.compare_digest(hash_value.encode(), password.encode())) + def _bcrypt(self, bcrypt: Any, hash_value: str, password: str) -> tuple[str, bool]: - return ("BCRYPT", bcrypt.checkpw(password=password.encode('utf-8'), hashed_password=hash_value.encode())) + if self._encryption == "autodetect" and len(hash_value) != 60: + return self._plain_fallback("BCRYPT", hash_value, password) + else: + return ("BCRYPT", bcrypt.checkpw(password=password.encode('utf-8'), hashed_password=hash_value.encode())) def _md5apr1(self, hash_value: str, password: str) -> tuple[str, bool]: - return ("MD5-APR1", apr_md5_crypt.verify(password, hash_value.strip())) + if self._encryption == "autodetect" and len(hash_value) != 37: + return self._plain_fallback("MD5-APR1", hash_value, password) + else: + return ("MD5-APR1", apr_md5_crypt.verify(password, hash_value.strip())) def _sha256(self, hash_value: str, password: str) -> tuple[str, bool]: - return ("SHA-256", sha256_crypt.verify(password, hash_value.strip())) + if self._encryption == "autodetect" and len(hash_value) != 63: + return self._plain_fallback("SHA-256", hash_value, password) + else: + return ("SHA-256", sha256_crypt.verify(password, hash_value.strip())) def _sha512(self, hash_value: str, password: str) -> tuple[str, bool]: - return ("SHA-512", sha512_crypt.verify(password, hash_value.strip())) + if self._encryption == "autodetect" and len(hash_value) != 106: + return self._plain_fallback("SHA-512", hash_value, password) + else: + return ("SHA-512", sha512_crypt.verify(password, hash_value.strip())) def _autodetect(self, hash_value: str, password: str) -> tuple[str, bool]: - if hash_value.startswith("$apr1$", 0, 6) and len(hash_value) == 37: + if hash_value.startswith("$apr1$", 0, 6): # MD5-APR1 return self._md5apr1(hash_value, password) - elif hash_value.startswith("$2y$", 0, 4) and len(hash_value) == 60: + elif re.match(r"^\$2(a|b|x|y)?\$", hash_value): # BCRYPT return self._verify_bcrypt(hash_value, password) - elif hash_value.startswith("$5$", 0, 3) and len(hash_value) == 63: + elif hash_value.startswith("$5$", 0, 3): # SHA-256 return self._sha256(hash_value, password) - elif hash_value.startswith("$6$", 0, 3) and len(hash_value) == 106: + elif hash_value.startswith("$6$", 0, 3): # SHA-512 return self._sha512(hash_value, password) else: - # assumed plaintext return self._plain(hash_value, password) def _read_htpasswd(self, init: bool, suppress: bool) -> Tuple[bool, int, dict, int, int]: From 3963bb4d8238d8f5bae7ce2cbcb8a8c89fce6203 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 9 Mar 2025 08:50:53 +0100 Subject: [PATCH 2/4] extend logging, adjust loglevel for hash error --- radicale/auth/htpasswd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/radicale/auth/htpasswd.py b/radicale/auth/htpasswd.py index bdec74c1..11700db9 100644 --- a/radicale/auth/htpasswd.py +++ b/radicale/auth/htpasswd.py @@ -306,13 +306,13 @@ class Auth(auth.BaseAuth): try: (method, password_ok) = self._verify(digest, password) except ValueError as e: - logger.warning("Login verification failed for user: '%s' (method '%s') '%s'", login, self._encryption, e) + logger.error("Login verification failed for user: '%s' (htpasswd/%s) with errror '%s'", login, self._encryption, e) return "" - logger.debug("Login verification successful for user: '%s' (method '%s')", login, method) if password_ok: + logger.debug("Login verification successful for user: '%s' (htpasswd/%s/%s)", login, self._encryption, method) return login else: - logger.warning("Login verification failed for user: '%s' (method '%s')", login, method) + logger.warning("Login verification failed for user: '%s' (htpasswd/%s/%s)", login, self._encryption, method) else: - logger.warning("Login verification user not found: '%s'", login) + logger.warning("Login verification user not found (htpasswd): '%s'", login) return "" From 9f0385fd67c2b4db0dafe366f10951b0fcbcb73c Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 9 Mar 2025 08:51:20 +0100 Subject: [PATCH 3/4] add some autodetect cases, add 2 additional bcrypt algo --- radicale/tests/test_auth.py | 48 +++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/radicale/tests/test_auth.py b/radicale/tests/test_auth.py index 6c356f97..b712e13d 100644 --- a/radicale/tests/test_auth.py +++ b/radicale/tests/test_auth.py @@ -79,6 +79,9 @@ class TestBaseAuthRequests(BaseTest): def test_htpasswd_plain(self) -> None: self._test_htpasswd("plain", "tmp:bepo") + def test_htpasswd_plain_autodetect(self) -> None: + self._test_htpasswd("autodetect", "tmp:bepo") + def test_htpasswd_plain_password_split(self) -> None: self._test_htpasswd("plain", "tmp:be:po", ( ("tmp", "be:po", True), ("tmp", "bepo", False))) @@ -89,6 +92,9 @@ class TestBaseAuthRequests(BaseTest): def test_htpasswd_md5(self) -> None: self._test_htpasswd("md5", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/") + def test_htpasswd_md5_autodetect(self) -> None: + self._test_htpasswd("autodetect", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/") + def test_htpasswd_md5_unicode(self): self._test_htpasswd( "md5", "😀:$apr1$w4ev89r1$29xO8EvJmS2HEAadQ5qy11", "unicode") @@ -96,18 +102,50 @@ class TestBaseAuthRequests(BaseTest): def test_htpasswd_sha256(self) -> None: self._test_htpasswd("sha256", "tmp:$5$i4Ni4TQq6L5FKss5$ilpTjkmnxkwZeV35GB9cYSsDXTALBn6KtWRJAzNlCL/") + def test_htpasswd_sha256_autodetect(self) -> None: + self._test_htpasswd("autodetect", "tmp:$5$i4Ni4TQq6L5FKss5$ilpTjkmnxkwZeV35GB9cYSsDXTALBn6KtWRJAzNlCL/") + def test_htpasswd_sha512(self) -> None: self._test_htpasswd("sha512", "tmp:$6$3Qhl8r6FLagYdHYa$UCH9yXCed4A.J9FQsFPYAOXImzZUMfvLa0lwcWOxWYLOF5sE/lF99auQ4jKvHY2vijxmefl7G6kMqZ8JPdhIJ/") + def test_htpasswd_sha512_autodetect(self) -> None: + self._test_htpasswd("autodetect", "tmp:$6$3Qhl8r6FLagYdHYa$UCH9yXCed4A.J9FQsFPYAOXImzZUMfvLa0lwcWOxWYLOF5sE/lF99auQ4jKvHY2vijxmefl7G6kMqZ8JPdhIJ/") + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") - def test_htpasswd_bcrypt(self) -> None: - self._test_htpasswd("bcrypt", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3V" - "NTRI3w5KDnj8NTUKJNWfVpvRq") + def test_htpasswd_bcrypt_2a(self) -> None: + self._test_htpasswd("bcrypt", "tmp:$2a$10$Mj4A9vMecAp/K7.0fMKoVOk1SjgR.RBhl06a52nvzXhxlT3HB7Reu") + + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") + def test_htpasswd_bcrypt_2a_autodetect(self) -> None: + self._test_htpasswd("autodetect", "tmp:$2a$10$Mj4A9vMecAp/K7.0fMKoVOk1SjgR.RBhl06a52nvzXhxlT3HB7Reu") + + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") + def test_htpasswd_bcrypt_2b(self) -> None: + self._test_htpasswd("bcrypt", "tmp:$2b$12$7a4z/fdmXlBIfkz0smvzW.1Nds8wpgC/bo2DVOb4OSQKWCDL1A1wu") + + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") + def test_htpasswd_bcrypt_2b_autodetect(self) -> None: + self._test_htpasswd("autodetect", "tmp:$2b$12$7a4z/fdmXlBIfkz0smvzW.1Nds8wpgC/bo2DVOb4OSQKWCDL1A1wu") + + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") + def test_htpasswd_bcrypt_2y(self) -> None: + self._test_htpasswd("bcrypt", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq") + + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") + def test_htpasswd_bcrypt_2y_autodetect(self) -> None: + self._test_htpasswd("autodetect", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq") + + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") + def test_htpasswd_bcrypt_C10(self) -> None: + self._test_htpasswd("bcrypt", "tmp:$2y$10$bZsWq06ECzxqi7RmulQvC.T1YHUnLW2E3jn.MU2pvVTGn1dfORt2a") + + @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") + def test_htpasswd_bcrypt_C10_autodetect(self) -> None: + self._test_htpasswd("bcrypt", "tmp:$2y$10$bZsWq06ECzxqi7RmulQvC.T1YHUnLW2E3jn.MU2pvVTGn1dfORt2a") @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") def test_htpasswd_bcrypt_unicode(self) -> None: - self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK" - "6U9Sqlzr.W1mMVCS8wJUftnW", "unicode") + self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode") def test_htpasswd_multi(self) -> None: self._test_htpasswd("plain", "ign:ign\ntmp:bepo") From cf727101f87bc384c4d6c7631560d226c5e8562e Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 9 Mar 2025 08:53:49 +0100 Subject: [PATCH 4/4] update related to htpasswd auth --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a2babf1..a156950e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Adjust: [auth] imap: use AUTHENTICATE PLAIN instead of LOGIN towards remote IMAP server * Improve: log client IP on SSL error and SSL protocol+cipher if successful * Improve: catch htpasswd hash verification errors +* Improve: add support for more bcrypt algos on autodetection, extend logging for autodetection fallback to PLAIN in case of hash length is not matching ## 3.4.1 * Add: option [auth] dovecot_connection_type / dovecot_host / dovecot_port