1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-06-26 16:45:52 +00:00

Merge pull request #1729 from pbiering/htpasswd-cosmetic-bcrypt-extensions

Htpasswd cosmetic bcrypt extensions
This commit is contained in:
Peter Bieringer 2025-03-09 08:59:24 +01:00 committed by GitHub
commit 0f67336987
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 74 additions and 18 deletions

View file

@ -16,6 +16,7 @@
* Adjust: [auth] imap: use AUTHENTICATE PLAIN instead of LOGIN towards remote IMAP server * 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: log client IP on SSL error and SSL protocol+cipher if successful
* Improve: catch htpasswd hash verification errors * 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 ## 3.4.1
* Add: option [auth] dovecot_connection_type / dovecot_host / dovecot_port * Add: option [auth] dovecot_connection_type / dovecot_host / dovecot_port

View file

@ -51,6 +51,7 @@ When bcrypt is installed:
import functools import functools
import hmac import hmac
import os import os
import re
import threading import threading
import time import time
from typing import Any, Tuple from typing import Any, Tuple
@ -131,33 +132,49 @@ class Auth(auth.BaseAuth):
"""Check if ``hash_value`` and ``password`` match, plain method.""" """Check if ``hash_value`` and ``password`` match, plain method."""
return ("PLAIN", hmac.compare_digest(hash_value.encode(), password.encode())) 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]: 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]: 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]: 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]: 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]: 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 # MD5-APR1
return self._md5apr1(hash_value, password) 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 # BCRYPT
return self._verify_bcrypt(hash_value, password) 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 # SHA-256
return self._sha256(hash_value, password) 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 # SHA-512
return self._sha512(hash_value, password) return self._sha512(hash_value, password)
else: else:
# assumed plaintext
return self._plain(hash_value, password) return self._plain(hash_value, password)
def _read_htpasswd(self, init: bool, suppress: bool) -> Tuple[bool, int, dict, int, int]: def _read_htpasswd(self, init: bool, suppress: bool) -> Tuple[bool, int, dict, int, int]:
@ -289,13 +306,13 @@ class Auth(auth.BaseAuth):
try: try:
(method, password_ok) = self._verify(digest, password) (method, password_ok) = self._verify(digest, password)
except ValueError as e: 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 "" return ""
logger.debug("Login verification successful for user: '%s' (method '%s')", login, method)
if password_ok: if password_ok:
logger.debug("Login verification successful for user: '%s' (htpasswd/%s/%s)", login, self._encryption, method)
return login return login
else: 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: else:
logger.warning("Login verification user not found: '%s'", login) logger.warning("Login verification user not found (htpasswd): '%s'", login)
return "" return ""

View file

@ -79,6 +79,9 @@ class TestBaseAuthRequests(BaseTest):
def test_htpasswd_plain(self) -> None: def test_htpasswd_plain(self) -> None:
self._test_htpasswd("plain", "tmp:bepo") 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: def test_htpasswd_plain_password_split(self) -> None:
self._test_htpasswd("plain", "tmp:be:po", ( self._test_htpasswd("plain", "tmp:be:po", (
("tmp", "be:po", True), ("tmp", "bepo", False))) ("tmp", "be:po", True), ("tmp", "bepo", False)))
@ -89,6 +92,9 @@ class TestBaseAuthRequests(BaseTest):
def test_htpasswd_md5(self) -> None: def test_htpasswd_md5(self) -> None:
self._test_htpasswd("md5", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/") 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): def test_htpasswd_md5_unicode(self):
self._test_htpasswd( self._test_htpasswd(
"md5", "😀:$apr1$w4ev89r1$29xO8EvJmS2HEAadQ5qy11", "unicode") "md5", "😀:$apr1$w4ev89r1$29xO8EvJmS2HEAadQ5qy11", "unicode")
@ -96,18 +102,50 @@ class TestBaseAuthRequests(BaseTest):
def test_htpasswd_sha256(self) -> None: def test_htpasswd_sha256(self) -> None:
self._test_htpasswd("sha256", "tmp:$5$i4Ni4TQq6L5FKss5$ilpTjkmnxkwZeV35GB9cYSsDXTALBn6KtWRJAzNlCL/") 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: def test_htpasswd_sha512(self) -> None:
self._test_htpasswd("sha512", "tmp:$6$3Qhl8r6FLagYdHYa$UCH9yXCed4A.J9FQsFPYAOXImzZUMfvLa0lwcWOxWYLOF5sE/lF99auQ4jKvHY2vijxmefl7G6kMqZ8JPdhIJ/") 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") @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed")
def test_htpasswd_bcrypt(self) -> None: def test_htpasswd_bcrypt_2a(self) -> None:
self._test_htpasswd("bcrypt", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3V" self._test_htpasswd("bcrypt", "tmp:$2a$10$Mj4A9vMecAp/K7.0fMKoVOk1SjgR.RBhl06a52nvzXhxlT3HB7Reu")
"NTRI3w5KDnj8NTUKJNWfVpvRq")
@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") @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed")
def test_htpasswd_bcrypt_unicode(self) -> None: def test_htpasswd_bcrypt_unicode(self) -> None:
self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK" self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode")
"6U9Sqlzr.W1mMVCS8wJUftnW", "unicode")
def test_htpasswd_multi(self) -> None: def test_htpasswd_multi(self) -> None:
self._test_htpasswd("plain", "ign:ign\ntmp:bepo") self._test_htpasswd("plain", "ign:ign\ntmp:bepo")