Merge pull request #1955 from pbiering/passlib-libpass-check-warn

Passlib libpass check related to bcrypt
This commit is contained in:
Peter Bieringer
2026-01-06 17:55:54 +01:00
committed by GitHub
5 changed files with 69 additions and 12 deletions

View File

@@ -1037,6 +1037,7 @@ Available methods:
* `bcrypt`
This uses a modified version of the Blowfish stream cipher, which is considered very secure.
The installation of Python's **bcrypt** module is required for this to work.
Also consider version of passlib(libpass): bcrypt >= 5.0.0 requires passlib(libpass) >= 1.9.3
* `md5`
Use an iterated MD5 digest of the password with salt (nowadays insecure).

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2019 Unrud <unrud@outlook.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
# Copyright © 2024-2026 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
@@ -43,7 +43,7 @@ out-of-the-box:
- SHA256 (htpasswd -2 ...)
- SHA512 (htpasswd -5 ...)
When bcrypt is installed:
When bcrypt is installed (bcrypt >= 5.0.0 requires passlib/libpass >= 1.9.3):
- BCRYPT (htpasswd -B ...) -- Requires htpasswd 2.4.x
When argon2 is installed:
@@ -61,7 +61,7 @@ from typing import Any, Tuple
from passlib.hash import apr_md5_crypt, sha256_crypt, sha512_crypt
from radicale import auth, config, logger
from radicale import auth, config, logger, utils
class Auth(auth.BaseAuth):
@@ -120,12 +120,22 @@ class Auth(auth.BaseAuth):
"The htpasswd encryption method 'bcrypt' or 'autodetect' requires "
"the bcrypt module (entries found: %d)." % self._htpasswd_bcrypt_use) from e
else:
self._has_bcrypt = True
[bcrypt_usable, info] = utils.passlib_libpass_supports_bcrypt()
if bcrypt_usable:
self._has_bcrypt = True
logger.debug(info)
else:
logger.warning(info)
if self._encryption == "autodetect":
if self._htpasswd_bcrypt_use == 0:
logger.info("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s' and bycrypt module found, but currently not required", self._encryption)
logger.info("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s' and bcrypt module found, but currently not required", self._encryption)
else:
logger.info("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s' and bycrypt module found (bcrypt entries found: %d)", self._encryption, self._htpasswd_bcrypt_use)
logger.info("auth htpasswd encryption is 'radicale.auth.htpasswd_encryption.%s' and bcrypt module found (bcrypt entries found: %d)", self._encryption, self._htpasswd_bcrypt_use)
if not bcrypt_usable:
raise RuntimeError("The htpasswd encryption 'autodetect' requires the bcrypt module but not usuable")
else:
if not bcrypt_usable:
raise RuntimeError("The htpasswd encryption method 'bcrypt' requires the bcrypt module but not usuable")
if self._encryption == "bcrypt":
self._verify = functools.partial(self._bcrypt, bcrypt)
else:

View File

@@ -1,7 +1,7 @@
# This file is part of Radicale - CalDAV and CardDAV server
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2023 Unrud <unrud@outlook.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
# Copyright © 2024-2026 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
@@ -23,6 +23,8 @@ Tests for Radicale.
import base64
import logging
import os
import platform
import shutil
import sys
import tempfile
@@ -36,7 +38,7 @@ import defusedxml.ElementTree as DefusedET
import vobject
import radicale
from radicale import app, config, types, xmlutils
from radicale import app, config, types, utils, xmlutils
RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]], vobject.base.Component]]
@@ -52,6 +54,11 @@ class BaseTest:
application: app.Application
def setup_method(self) -> None:
if os.environ.get("PYTHONPATH"):
info = "with PYTHONPATH=%r " % os.environ.get("PYTHONPATH")
else:
info = ""
logging.info("Testing Radicale %s(%s) as %s on %s", info, utils.packages_version(), utils.user_groups_as_string(), platform.platform())
self.configuration = config.load()
self.colpath = tempfile.mkdtemp()
self.configure({

View File

@@ -2,7 +2,7 @@
# Copyright © 2012-2016 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2022 Unrud <unrud@outlook.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
# Copyright © 2024-2026 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
@@ -30,7 +30,7 @@ from typing import Iterable, Tuple, Union
import pytest
from radicale import xmlutils
from radicale import utils, xmlutils
from radicale.tests import BaseTest
@@ -121,38 +121,47 @@ class TestBaseAuthRequests(BaseTest):
self._test_htpasswd("autodetect", "tmp:$6$3Qhl8r6FLagYdHYa$UCH9yXCed4A.J9FQsFPYAOXImzZUMfvLa0lwcWOxWYLOF5sE/lF99auQ4jKvHY2vijxmefl7G6kMqZ8JPdhIJ/")
@pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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")
@pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed or incompatibe")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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")
@pytest.mark.skipif(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
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(not utils.passlib_libpass_supports_bcrypt()[0], reason="bcrypt module incompatible with passlib(libpass) module")
def test_htpasswd_bcrypt_unicode(self) -> None:
self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode")

View File

@@ -2,7 +2,7 @@
# Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
# Copyright © 2024-2026 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
@@ -27,6 +27,8 @@ from importlib import import_module, metadata
from string import ascii_letters, digits, punctuation
from typing import Callable, Sequence, Tuple, Type, TypeVar, Union
from packaging.version import Version
from radicale import config
from radicale.log import logger
@@ -85,6 +87,10 @@ def load_plugin(internal_types: Sequence[str], module_name: str,
def package_version(name):
if name == "passlib":
# passlib(libpass) requires special handling as module name is unchanged, but metadata has new name
import passlib
return passlib.__version__
return metadata.version(name)
@@ -99,6 +105,30 @@ def vobject_supports_vcard4() -> bool:
return False
def passlib_libpass_supports_bcrypt() -> Tuple[bool, str]:
"""Check if passlib/libpass version supports bcrypt version."""
info = ""
try:
version_bcrypt = package_version("bcrypt")
version_bcrypt_check = "5.0.0"
version_passlib = package_version("passlib")
version_passlib_check = "1.9.3"
if Version(version_bcrypt) >= Version(version_bcrypt_check):
# bcrypt >= 5.0.0 has issues with passlib(libpass) < 1.9.3
if Version(version_passlib) < Version(version_passlib_check):
info = "bcrypt module version %r >= %r and passlib(libpass) module version %r < %r found => incompatible, downgrade bcrypt or upgrade passlib(libpass)" % (version_bcrypt, version_bcrypt_check, version_passlib, version_passlib_check)
return (False, info)
else:
info = "bcrypt module version %r >= %r and passlib(libpass) module version %r >= %r found => ok" % (version_bcrypt, version_bcrypt_check, version_passlib, version_passlib_check)
return (True, info)
else:
info = "bcrypt module version %r < %r and passlib(libpass) module version %r found => ok" % (version_bcrypt, version_bcrypt_check, version_passlib)
return (True, info)
except Exception:
info = "bcrypt module version or passlib(libpass) module version %r not found => problem"
return (False, info)
def packages_version():
versions = []
versions.append("python=%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2]))