From 401b68fe0860284dc14e539917d08350900f4ed4 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Mar 2024 06:08:26 +0100 Subject: [PATCH] extend htpasswd_encryption options with sha256/512/autodetect --- radicale/auth/htpasswd.py | 56 ++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/radicale/auth/htpasswd.py b/radicale/auth/htpasswd.py index 8bba1f780..098f7b783 100644 --- a/radicale/auth/htpasswd.py +++ b/radicale/auth/htpasswd.py @@ -3,6 +3,7 @@ # Copyright © 2008 Pascal Halter # Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2017-2019 Unrud +# Copyright © 2024 Peter Bieringer # # 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 @@ 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 @@ 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 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 @@ def __init__(self, configuration: config.Configuration) -> None: 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 @@ def _bcrypt(self, bcrypt: Any, hash_value: str, password: str) -> bool: 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.