Просмотр исходного кода

extend htpasswd_encryption options with sha256/512/autodetect

Peter Bieringer 1 год назад
Родитель
Сommit
401b68fe08
1 измененных файлов с 43 добавлено и 13 удалено
  1. 43 13
      radicale/auth/htpasswd.py

+ 43 - 13
radicale/auth/htpasswd.py

@@ -3,6 +3,7 @@
 # Copyright © 2008 Pascal Halter
 # Copyright © 2008-2017 Guillaume Ayoub
 # Copyright © 2017-2019 Unrud <unrud@outlook.com>
+# Copyright © 2024 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -22,12 +23,12 @@ Authentication backend that checks credentials with a htpasswd file.
 
 Apache's htpasswd command (httpd.apache.org/docs/programs/htpasswd.html)
 manages a file for storing user credentials. It can encrypt passwords using
-different the methods BCRYPT or MD5-APR1 (a version of MD5 modified for
-Apache). MD5-APR1 provides medium security as of 2015. Only BCRYPT can be
+different the methods BCRYPT/SHA256/SHA512 or MD5-APR1 (a version of MD5 modified for
+Apache). MD5-APR1 provides medium security as of 2015. Only BCRYPT/SHA256/SHA512 can be
 considered secure by current standards.
 
 MD5-APR1-encrypted credentials can be written by all versions of htpasswd (it
-is the default, in fact), whereas BCRYPT requires htpasswd 2.4.x or newer.
+is the default, in fact), whereas BCRYPT/SHA256/SHA512 requires htpasswd 2.4.x or newer.
 
 The `is_authenticated(user, password)` function provided by this module
 verifies the user-given credentials by parsing the htpasswd credential file
@@ -37,13 +38,13 @@ configuration value.
 
 The following htpasswd password encrpytion methods are supported by Radicale
 out-of-the-box:
-
-    - plain-text (created by htpasswd -p...) -- INSECURE
-    - MD5-APR1   (htpasswd -m...) -- htpasswd's default method
+    - plain-text (created by htpasswd -p ...) -- INSECURE
+    - MD5-APR1   (htpasswd -m ...) -- htpasswd's default method, INSECURE
+    - SHA256     (htpasswd -2 ...)
+    - SHA512     (htpasswd -5 ...)
 
 When bcrypt is installed:
-
-    - BCRYPT     (htpasswd -B...) -- Requires htpasswd 2.4.x
+    - BCRYPT     (htpasswd -B ...) -- Requires htpasswd 2.4.x
 
 """
 
@@ -51,9 +52,9 @@ import functools
 import hmac
 from typing import Any
 
-from passlib.hash import apr_md5_crypt
+from passlib.hash import apr_md5_crypt, sha256_crypt, sha512_crypt
 
-from radicale import auth, config
+from radicale import auth, config, logger
 
 
 class Auth(auth.BaseAuth):
@@ -67,18 +68,24 @@ class Auth(auth.BaseAuth):
         self._encoding = configuration.get("encoding", "stock")
         encryption: str = configuration.get("auth", "htpasswd_encryption")
 
+        logger.info("auth password encryption: %s", encryption)
+
         if encryption == "plain":
             self._verify = self._plain
         elif encryption == "md5":
             self._verify = self._md5apr1
-        elif encryption == "bcrypt":
+        elif encryption == "bcrypt" or encryption == "autodetect":
             try:
                 import bcrypt
             except ImportError as e:
                 raise RuntimeError(
-                    "The htpasswd encryption method 'bcrypt' requires "
+                    "The htpasswd encryption method 'bcrypt' or 'auto' requires "
                     "the bcrypt module.") from e
-            self._verify = functools.partial(self._bcrypt, bcrypt)
+            if encryption == "bcrypt":
+                self._verify = functools.partial(self._bcrypt, bcrypt)
+            else:
+                self._verify = self._autodetect
+                self._verify_bcrypt = functools.partial(self._bcrypt, bcrypt)
         else:
             raise RuntimeError("The htpasswd encryption method %r is not "
                                "supported." % encryption)
@@ -93,6 +100,29 @@ class Auth(auth.BaseAuth):
     def _md5apr1(self, hash_value: str, password: str) -> bool:
         return apr_md5_crypt.verify(password, hash_value.strip())
 
+    def _sha256(self, hash_value: str, password: str) -> bool:
+        return sha256_crypt.verify(password, hash_value.strip())
+
+    def _sha512(self, hash_value: str, password: str) -> bool:
+        return sha512_crypt.verify(password, hash_value.strip())
+
+    def _autodetect(self, hash_value: str, password: str) -> bool:
+        if hash_value.startswith("$apr1$", 0, 6) and len(hash_value) == 37:
+            # MD5-APR1
+            return self._md5apr1(hash_value, password)
+        elif hash_value.startswith("$2y$", 0, 4) and len(hash_value) == 60:
+            # BCRYPT
+            return self._verify_bcrypt(hash_value, password)
+        elif hash_value.startswith("$5$", 0, 3) and len(hash_value) == 63:
+            # SHA-256
+            return self._sha256(hash_value, password)
+        elif hash_value.startswith("$6$", 0, 3) and len(hash_value) == 106:
+            # SHA-512
+            return self._sha512(hash_value, password)
+        else:
+            # assumed plaintext
+            return self._plain(hash_value, password)
+
     def login(self, login: str, password: str) -> str:
         """Validate credentials.