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` * `bcrypt`
This uses a modified version of the Blowfish stream cipher, which is considered very secure. 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. 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` * `md5`
Use an iterated MD5 digest of the password with salt (nowadays insecure). Use an iterated MD5 digest of the password with salt (nowadays insecure).

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2019 Unrud <unrud@outlook.com> # 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 # 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 # it under the terms of the GNU General Public License as published by
@@ -43,7 +43,7 @@ out-of-the-box:
- SHA256 (htpasswd -2 ...) - SHA256 (htpasswd -2 ...)
- SHA512 (htpasswd -5 ...) - 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 - BCRYPT (htpasswd -B ...) -- Requires htpasswd 2.4.x
When argon2 is installed: 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 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): class Auth(auth.BaseAuth):
@@ -120,12 +120,22 @@ class Auth(auth.BaseAuth):
"The htpasswd encryption method 'bcrypt' or 'autodetect' requires " "The htpasswd encryption method 'bcrypt' or 'autodetect' requires "
"the bcrypt module (entries found: %d)." % self._htpasswd_bcrypt_use) from e "the bcrypt module (entries found: %d)." % self._htpasswd_bcrypt_use) from e
else: 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._encryption == "autodetect":
if self._htpasswd_bcrypt_use == 0: 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: 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": if self._encryption == "bcrypt":
self._verify = functools.partial(self._bcrypt, bcrypt) self._verify = functools.partial(self._bcrypt, bcrypt)
else: else:

View File

@@ -1,7 +1,7 @@
# This file is part of Radicale - CalDAV and CardDAV server # This file is part of Radicale - CalDAV and CardDAV server
# Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2023 Unrud <unrud@outlook.com> # 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 # 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 # it under the terms of the GNU General Public License as published by
@@ -23,6 +23,8 @@ Tests for Radicale.
import base64 import base64
import logging import logging
import os
import platform
import shutil import shutil
import sys import sys
import tempfile import tempfile
@@ -36,7 +38,7 @@ import defusedxml.ElementTree as DefusedET
import vobject import vobject
import radicale 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]] RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]], vobject.base.Component]]
@@ -52,6 +54,11 @@ class BaseTest:
application: app.Application application: app.Application
def setup_method(self) -> None: 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.configuration = config.load()
self.colpath = tempfile.mkdtemp() self.colpath = tempfile.mkdtemp()
self.configure({ self.configure({

View File

@@ -2,7 +2,7 @@
# Copyright © 2012-2016 Jean-Marc Martins # Copyright © 2012-2016 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2022 Unrud <unrud@outlook.com> # 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 # 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 # 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 import pytest
from radicale import xmlutils from radicale import utils, xmlutils
from radicale.tests import BaseTest from radicale.tests import BaseTest
@@ -121,38 +121,47 @@ class TestBaseAuthRequests(BaseTest):
self._test_htpasswd("autodetect", "tmp:$6$3Qhl8r6FLagYdHYa$UCH9yXCed4A.J9FQsFPYAOXImzZUMfvLa0lwcWOxWYLOF5sE/lF99auQ4jKvHY2vijxmefl7G6kMqZ8JPdhIJ/") 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")
@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: def test_htpasswd_bcrypt_2a(self) -> None:
self._test_htpasswd("bcrypt", "tmp:$2a$10$Mj4A9vMecAp/K7.0fMKoVOk1SjgR.RBhl06a52nvzXhxlT3HB7Reu") 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: def test_htpasswd_bcrypt_2a_autodetect(self) -> None:
self._test_htpasswd("autodetect", "tmp:$2a$10$Mj4A9vMecAp/K7.0fMKoVOk1SjgR.RBhl06a52nvzXhxlT3HB7Reu") 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(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: def test_htpasswd_bcrypt_2b(self) -> None:
self._test_htpasswd("bcrypt", "tmp:$2b$12$7a4z/fdmXlBIfkz0smvzW.1Nds8wpgC/bo2DVOb4OSQKWCDL1A1wu") 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(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: def test_htpasswd_bcrypt_2b_autodetect(self) -> None:
self._test_htpasswd("autodetect", "tmp:$2b$12$7a4z/fdmXlBIfkz0smvzW.1Nds8wpgC/bo2DVOb4OSQKWCDL1A1wu") 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(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: def test_htpasswd_bcrypt_2y(self) -> None:
self._test_htpasswd("bcrypt", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq") self._test_htpasswd("bcrypt", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq")
@pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") @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: def test_htpasswd_bcrypt_2y_autodetect(self) -> None:
self._test_htpasswd("autodetect", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq") self._test_htpasswd("autodetect", "tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq")
@pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") @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: def test_htpasswd_bcrypt_C10(self) -> None:
self._test_htpasswd("bcrypt", "tmp:$2y$10$bZsWq06ECzxqi7RmulQvC.T1YHUnLW2E3jn.MU2pvVTGn1dfORt2a") 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")
@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: def test_htpasswd_bcrypt_C10_autodetect(self) -> None:
self._test_htpasswd("bcrypt", "tmp:$2y$10$bZsWq06ECzxqi7RmulQvC.T1YHUnLW2E3jn.MU2pvVTGn1dfORt2a") 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")
@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: def test_htpasswd_bcrypt_unicode(self) -> None:
self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode") self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode")

View File

@@ -2,7 +2,7 @@
# Copyright © 2014 Jean-Marc Martins # Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <unrud@outlook.com> # 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 # 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 # 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 string import ascii_letters, digits, punctuation
from typing import Callable, Sequence, Tuple, Type, TypeVar, Union from typing import Callable, Sequence, Tuple, Type, TypeVar, Union
from packaging.version import Version
from radicale import config from radicale import config
from radicale.log import logger from radicale.log import logger
@@ -85,6 +87,10 @@ def load_plugin(internal_types: Sequence[str], module_name: str,
def package_version(name): 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) return metadata.version(name)
@@ -99,6 +105,30 @@ def vobject_supports_vcard4() -> bool:
return False 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(): def packages_version():
versions = [] versions = []
versions.append("python=%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])) versions.append("python=%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2]))