Skip to content

Commit d400456

Browse files
committed
fix: emit warning / exception from 'crypt_check'
Point users to 'bcrypt.checkpw' as a better alternative. Closes #44
1 parent 5ebd674 commit d400456

File tree

4 files changed

+73
-11
lines changed

4 files changed

+73
-11
lines changed

docs/configuration.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ nominated to act as authenticator plugins. ::
232232
use = repoze.who.plugins.htpasswd:make_plugin
233233
filename = %(here)s/passwd
234234
check_fn = repoze.who.plugins.htpasswd:crypt_check
235+
# The stdlib 'crypt' module is not available for Python > 3.13. Instead,
236+
# use: 'bcrypt.checkpw' function (see https://pypi.org/projects/bcrypt/).
237+
# check_fn = bcrypt:checkpw
235238

236239
[plugin:sqlusers]
237240
# authentication

docs/plugins.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ authentication, identification, challenge and metadata provision.
133133
``repoze.who.plugins.htpasswd:crypt_check``; it assumes the values
134134
in the htpasswd file are encrypted with the UNIX ``crypt`` function.
135135

136+
.. note::
137+
The ``crypt`` module is not available in the standard library for
138+
Python >= 3.13. Recommended replacement for the checker function
139+
we provide here (``repoze.who.plugins.htpasswd:crypt_check``) is the
140+
``bcrypt.checkpw`` function (see https://pypi.org/projects/bcrypt/).
141+
136142
.. module:: repoze.who.plugins.redirector
137143

138144
.. class:: RedirectorPlugin(login_url, came_from_param, reason_param, reason_header)

repoze/who/plugins/htpasswd.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1+
try:
2+
import crypt
3+
except ImportError:
4+
# Note: the crypt module is deprecated since Python 3.11
5+
# and will be removed in Python 3.13.
6+
# win32 does not have a crypt library at all.
7+
HAS_CRYPT = False
8+
else:
9+
HAS_CRYPT = True
110
import itertools
11+
import warnings
212

313
from zope.interface import implementer
414

@@ -90,13 +100,27 @@ def _same_string(x, y):
90100
mismatches = list(mismatches)
91101
return len(mismatches) == 0
92102

103+
104+
if not HAS_CRYPT:
105+
106+
class CryptModuleNotImportable(RuntimeError):
107+
def __init__(self):
108+
super().__init__(
109+
"'crypt' module is not importable. "
110+
"Try 'bcrypt.checkpw' instead?"
111+
)
112+
93113
def crypt_check(password, hashed):
94-
# Note: the crypt module is deprecated since Python 3.11
95-
# and will be removed in Python 3.13.
96-
# win32 does not have a crypt library at all.
97-
from crypt import crypt
114+
115+
if not HAS_CRYPT:
116+
raise CryptModuleNotImportable()
117+
118+
warnings.warn(
119+
"'crypt' module is deprecated -- try 'bcrypt.checkpw' instead?"
120+
)
98121
salt = hashed[:2]
99-
return _same_string(hashed, crypt(password, salt))
122+
return _same_string(hashed, crypt.crypt(password, salt))
123+
100124

101125
def sha1_check(password, hashed):
102126
from hashlib import sha1

repoze/who/plugins/tests/test_htpasswd.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import unittest
2-
3-
41
try:
52
from crypt import crypt
63
except ImportError:
74
# The crypt module is deprecated since Python 3.11
85
# and will be removed in Python 3.13.
96
# win32 does not have a crypt library at all.
107
crypt = None
8+
import unittest
9+
import warnings
10+
11+
import pytest
1112

1213

1314
class TestHTPasswdPlugin(unittest.TestCase):
@@ -116,12 +117,40 @@ def warn(self, msg):
116117
self.assertTrue('could not open htpasswd' in logger.warnings[0])
117118

118119
@unittest.skipIf(crypt is None, "crypt module not available")
119-
def test_crypt_check(self):
120+
def test_crypt_check_hit(self):
121+
from repoze.who.plugins.htpasswd import crypt_check
120122
salt = '123'
121123
hashed = crypt('password', salt)
124+
125+
with warnings.catch_warnings(record=True) as logged:
126+
assert crypt_check('password', hashed)
127+
128+
assert len(logged) == 1
129+
record = logged[0]
130+
assert record.category is UserWarning
131+
assert "'crypt' module is deprecated" in str(record.message)
132+
133+
@unittest.skipIf(crypt is None, "crypt module not available")
134+
def test_crypt_check_miss(self):
135+
from repoze.who.plugins.htpasswd import crypt_check
136+
salt = '123'
137+
hashed = crypt('password', salt)
138+
139+
with warnings.catch_warnings(record=True) as logged:
140+
assert not crypt_check('notpassword', hashed)
141+
142+
assert len(logged) == 1
143+
record = logged[0]
144+
assert record.category is UserWarning
145+
assert "'crypt' module is deprecated" in str(record.message)
146+
147+
@unittest.skipIf(crypt is not None, "crypt module available")
148+
def test_crypt_check_gone(self):
149+
from repoze.who.plugins.htpasswd import CryptModuleNotImportable
122150
from repoze.who.plugins.htpasswd import crypt_check
123-
self.assertEqual(crypt_check('password', hashed), True)
124-
self.assertEqual(crypt_check('notpassword', hashed), False)
151+
152+
with pytest.raises(CryptModuleNotImportable):
153+
crypt_check('password', 'hashed')
125154

126155
def test_sha1_check_w_password_str(self):
127156
from base64 import standard_b64encode

0 commit comments

Comments
 (0)