4646When bcrypt is installed:
4747 - BCRYPT (htpasswd -B ...) -- Requires htpasswd 2.4.x
4848
49+ When argon2 is installed:
50+ - ARGON2 (python -c 'from passlib.hash import argon2; print(argon2.using(type="ID").hash("password"))')
51+
4952"""
5053
5154import functools
@@ -72,8 +75,10 @@ class Auth(auth.BaseAuth):
7275 _htpasswd_not_ok_time : float
7376 _htpasswd_not_ok_reminder_seconds : int
7477 _htpasswd_bcrypt_use : int
78+ _htpasswd_argon2_use : int
7579 _htpasswd_cache : bool
7680 _has_bcrypt : bool
81+ _has_argon2 : bool
7782 _encryption : str
7883 _lock : threading .Lock
7984
@@ -89,9 +94,10 @@ def __init__(self, configuration: config.Configuration) -> None:
8994 logger .info ("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s'" , self ._encryption )
9095
9196 self ._has_bcrypt = False
97+ self ._has_argon2 = False
9298 self ._htpasswd_ok = False
9399 self ._htpasswd_not_ok_reminder_seconds = 60 # currently hardcoded
94- (self ._htpasswd_ok , self ._htpasswd_bcrypt_use , self ._htpasswd , self ._htpasswd_size , self ._htpasswd_mtime_ns ) = self ._read_htpasswd (True , False )
100+ (self ._htpasswd_ok , self ._htpasswd_bcrypt_use , self ._htpasswd_argon2_use , self . _htpasswd , self ._htpasswd_size , self ._htpasswd_mtime_ns ) = self ._read_htpasswd (True , False )
95101 self ._lock = threading .Lock ()
96102
97103 if self ._encryption == "plain" :
@@ -102,7 +108,8 @@ def __init__(self, configuration: config.Configuration) -> None:
102108 self ._verify = self ._sha256
103109 elif self ._encryption == "sha512" :
104110 self ._verify = self ._sha512
105- elif self ._encryption == "bcrypt" or self ._encryption == "autodetect" :
111+
112+ if self ._encryption == "bcrypt" or self ._encryption == "autodetect" :
106113 try :
107114 import bcrypt
108115 except ImportError as e :
@@ -125,7 +132,33 @@ def __init__(self, configuration: config.Configuration) -> None:
125132 self ._verify = self ._autodetect
126133 if self ._htpasswd_bcrypt_use :
127134 self ._verify_bcrypt = functools .partial (self ._bcrypt , bcrypt )
128- else :
135+
136+ if self ._encryption == "argon2" or self ._encryption == "autodetect" :
137+ try :
138+ import argon2
139+ from passlib .hash import argon2 # noqa: F811
140+ except ImportError as e :
141+ if (self ._encryption == "autodetect" ) and (self ._htpasswd_argon2_use == 0 ):
142+ logger .warning ("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s' which can require argon2 module, but currently no entries found" , self ._encryption )
143+ else :
144+ raise RuntimeError (
145+ "The htpasswd encryption method 'argon2' or 'autodetect' requires "
146+ "the argon2 module (entries found: %d)." % self ._htpasswd_argon2_use ) from e
147+ else :
148+ self ._has_argon2 = True
149+ if self ._encryption == "autodetect" :
150+ if self ._htpasswd_argon2_use == 0 :
151+ logger .info ("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s' and argon2 module found, but currently not required" , self ._encryption )
152+ else :
153+ logger .info ("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s' and argon2 module found (argon2 entries found: %d)" , self ._encryption , self ._htpasswd_argon2_use )
154+ if self ._encryption == "argon2" :
155+ self ._verify = functools .partial (self ._argon2 , argon2 )
156+ else :
157+ self ._verify = self ._autodetect
158+ if self ._htpasswd_argon2_use :
159+ self ._verify_argon2 = functools .partial (self ._argon2 , argon2 )
160+
161+ if not hasattr (self , '_verify' ):
129162 raise RuntimeError ("The htpasswd encryption method %r is not "
130163 "supported." % self ._encryption )
131164
@@ -144,6 +177,9 @@ def _bcrypt(self, bcrypt: Any, hash_value: str, password: str) -> tuple[str, boo
144177 else :
145178 return ("BCRYPT" , bcrypt .checkpw (password = password .encode ('utf-8' ), hashed_password = hash_value .encode ()))
146179
180+ def _argon2 (self , argon2 : Any , hash_value : str , password : str ) -> tuple [str , bool ]:
181+ return ("ARGON2" , argon2 .verify (password , hash_value .strip ()))
182+
147183 def _md5apr1 (self , hash_value : str , password : str ) -> tuple [str , bool ]:
148184 if self ._encryption == "autodetect" and len (hash_value ) != 37 :
149185 return self ._plain_fallback ("MD5-APR1" , hash_value , password )
@@ -169,6 +205,9 @@ def _autodetect(self, hash_value: str, password: str) -> tuple[str, bool]:
169205 elif re .match (r"^\$2(a|b|x|y)?\$" , hash_value ):
170206 # BCRYPT
171207 return self ._verify_bcrypt (hash_value , password )
208+ elif re .match (r"^\$argon2(i|d|id)\$" , hash_value ):
209+ # ARGON2
210+ return self ._verify_argon2 (hash_value , password )
172211 elif hash_value .startswith ("$5$" , 0 , 3 ):
173212 # SHA-256
174213 return self ._sha256 (hash_value , password )
@@ -178,7 +217,7 @@ def _autodetect(self, hash_value: str, password: str) -> tuple[str, bool]:
178217 else :
179218 return self ._plain (hash_value , password )
180219
181- def _read_htpasswd (self , init : bool , suppress : bool ) -> Tuple [bool , int , dict , int , int ]:
220+ def _read_htpasswd (self , init : bool , suppress : bool ) -> Tuple [bool , int , int , dict , int , int ]:
182221 """Read htpasswd file
183222
184223 init == True: stop on error
@@ -189,6 +228,7 @@ def _read_htpasswd(self, init: bool, suppress: bool) -> Tuple[bool, int, dict, i
189228 """
190229 htpasswd_ok = True
191230 bcrypt_use = 0
231+ argon2_use = 0
192232 if (init is True ) or (suppress is True ):
193233 info = "Read"
194234 else :
@@ -237,6 +277,14 @@ def _read_htpasswd(self, init: bool, suppress: bool) -> Tuple[bool, int, dict, i
237277 logger .warning ("htpasswd file contains bcrypt digest login: '%s' (line: %d / ignored because module is not loaded)" , login , line_num )
238278 skip = True
239279 htpasswd_ok = False
280+ if re .match (r"^\$argon2(i|d|id)\$" , digest ):
281+ if init is True :
282+ argon2_use += 1
283+ else :
284+ if self ._has_argon2 is False :
285+ logger .warning ("htpasswd file contains argon2 digest login: '%s' (line: %d / ignored because module is not loaded)" , login , line_num )
286+ skip = True
287+ htpasswd_ok = False
240288 if skip is False :
241289 htpasswd [login ] = digest
242290 entries += 1
@@ -259,7 +307,7 @@ def _read_htpasswd(self, init: bool, suppress: bool) -> Tuple[bool, int, dict, i
259307 self ._htpasswd_not_ok_time = 0
260308 else :
261309 self ._htpasswd_not_ok_time = time .time ()
262- return (htpasswd_ok , bcrypt_use , htpasswd , htpasswd_size , htpasswd_mtime_ns )
310+ return (htpasswd_ok , bcrypt_use , argon2_use , htpasswd , htpasswd_size , htpasswd_mtime_ns )
263311
264312 def _login (self , login : str , password : str ) -> str :
265313 """Validate credentials.
@@ -280,7 +328,7 @@ def _login(self, login: str, password: str) -> str:
280328 htpasswd_size = os .stat (self ._filename ).st_size
281329 htpasswd_mtime_ns = os .stat (self ._filename ).st_mtime_ns
282330 if (htpasswd_size != self ._htpasswd_size ) or (htpasswd_mtime_ns != self ._htpasswd_mtime_ns ):
283- (self ._htpasswd_ok , self ._htpasswd_bcrypt_use , self ._htpasswd , self ._htpasswd_size , self ._htpasswd_mtime_ns ) = self ._read_htpasswd (False , False )
331+ (self ._htpasswd_ok , self ._htpasswd_bcrypt_use , self ._htpasswd_argon2_use , self . _htpasswd , self ._htpasswd_size , self ._htpasswd_mtime_ns ) = self ._read_htpasswd (False , False )
284332 self ._htpasswd_not_ok_time = 0
285333
286334 # log reminder of problemantic file every interval
@@ -298,7 +346,7 @@ def _login(self, login: str, password: str) -> str:
298346 login_ok = True
299347 else :
300348 # read file on every request
301- (htpasswd_ok , htpasswd_bcrypt_use , htpasswd , htpasswd_size , htpasswd_mtime_ns ) = self ._read_htpasswd (False , True )
349+ (htpasswd_ok , htpasswd_bcrypt_use , htpasswd_argon2_use , htpasswd , htpasswd_size , htpasswd_mtime_ns ) = self ._read_htpasswd (False , True )
302350 if htpasswd .get (login ):
303351 digest = htpasswd [login ]
304352 login_ok = True
@@ -307,7 +355,7 @@ def _login(self, login: str, password: str) -> str:
307355 try :
308356 (method , password_ok ) = self ._verify (digest , password )
309357 except ValueError as e :
310- logger .error ("Login verification failed for user: '%s' (htpasswd/%s) with errror '%s'" , login , self ._encryption , e )
358+ logger .error ("Login verification failed for user: '%s' (htpasswd/%s) with error '%s'" , login , self ._encryption , e )
311359 return ""
312360 if password_ok :
313361 logger .debug ("Login verification successful for user: '%s' (htpasswd/%s/%s)" , login , self ._encryption , method )
0 commit comments