summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-03-12 22:44:22 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-03-12 22:44:22 -0400
commite89ebdf93b92dc018bd3ee1542cc4416b5024ab4 (patch)
tree49afbf5441e910c2667dd0cb1e8a075467ad857d /passlib
parentca830cd76a655f20488aebd082aba1a320e230d0 (diff)
downloadpasslib-e89ebdf93b92dc018bd3ee1542cc4416b5024ab4.tar.gz
bcrypt work
* added code to shoehorn $2$-support wrapper for bcryptor backend * added PasslibSecurityWarning when builtin backend is enabled (still considered whether it should be enabled by default) * py3 compat fix for repair_unused
Diffstat (limited to 'passlib')
-rw-r--r--passlib/exc.py8
-rw-r--r--passlib/handlers/bcrypt.py89
-rw-r--r--passlib/tests/test_handlers.py42
-rw-r--r--passlib/utils/__init__.py5
4 files changed, 101 insertions, 43 deletions
diff --git a/passlib/exc.py b/passlib/exc.py
index 0192951..cb158e7 100644
--- a/passlib/exc.py
+++ b/passlib/exc.py
@@ -56,6 +56,14 @@ class PasslibRuntimeWarning(PasslibWarning):
that the developers would love to hear under what conditions it occurred.
"""
+class PasslibSecurityWarning(PasslibWarning):
+ """Special warning issued when Passlib encounters something
+ that might affect security.
+
+ The main reason this is issued is when Passlib's pure-python bcrypt
+ backend is used, to warn that it's 20x too slow to acheive real security.
+ """
+
#==========================================================================
# eof
#==========================================================================
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index efe42f4..a1270e2 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -4,7 +4,7 @@ Implementation of OpenBSD's BCrypt algorithm.
TODO:
-* support 2x and altered-2a hashes?
+* support 2x and altered-2a hashes?
http://www.openwall.com/lists/oss-security/2011/06/27/9
* is there any workaround for bcryptor lacking $2$ support?
@@ -23,17 +23,17 @@ from warnings import warn
#site
try:
from bcrypt import hashpw as pybcrypt_hashpw
-except ImportError: #pragma: no cover - though should run whole suite w/o pybcrypt installed
+except ImportError: #pragma: no cover
pybcrypt_hashpw = None
try:
from bcryptor.engine import Engine as bcryptor_engine
-except ImportError: #pragma: no cover - though should run whole suite w/o bcryptor installed
+except ImportError: #pragma: no cover
bcryptor_engine = None
#libs
-from passlib.exc import PasslibHashWarning
+from passlib.exc import PasslibHashWarning, PasslibSecurityWarning
from passlib.utils import bcrypt64, safe_crypt, \
classproperty, rng, getrandstr, test_crypt
-from passlib.utils.compat import bytes, u, uascii_to_str, unicode
+from passlib.utils.compat import bytes, u, uascii_to_str, unicode, str_to_uascii
import passlib.utils.handlers as uh
#pkg
@@ -49,6 +49,7 @@ def _load_builtin():
if _builtin_bcrypt is None:
from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt
+IDENT_2 = u("$2$")
IDENT_2A = u("$2a$")
IDENT_2X = u("$2x$")
IDENT_2Y = u("$2y$")
@@ -111,7 +112,7 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
#NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap
#--HasRounds--
- default_rounds = 12 #current passlib default
+ default_rounds = 12 # current passlib default
min_rounds = 4 # bcrypt spec specified minimum
max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)
rounds_cost = "log2"
@@ -123,6 +124,9 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
@classmethod
def from_string(cls, hash):
ident, tail = cls._parse_ident(hash)
+ if ident == IDENT_2X:
+ raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
+ "currently supported")
rounds, data = tail.split(u("$"))
rval = int(rounds)
if rounds != u('%02d') % (rval,):
@@ -135,19 +139,22 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
ident=ident,
)
- def to_string(self, _for_backend=False):
- ident = self.ident
- if _for_backend and ident == IDENT_2Y:
- # hack so we can pass 2y strings to pybcrypt etc,
- # which only honors 2/2a.
- ident = IDENT_2A
- elif ident == IDENT_2X:
- raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
- "currently supported")
- hash = u("%s%02d$%s%s") % (ident, self.rounds, self.salt,
+ def to_string(self):
+ hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt,
self.checksum or u(''))
return uascii_to_str(hash)
+ def _get_config(self, ident=None):
+ "internal helper to prepare config string for backends"
+ if ident is None:
+ ident = self.ident
+ if ident == IDENT_2Y:
+ ident = IDENT_2A
+ else:
+ assert ident != IDENT_2X
+ config = u("%s%02d$%s") % (ident, self.rounds, self.salt)
+ return uascii_to_str(config)
+
#=========================================================
# specialized salt generation - fixes passlib issue 25
#=========================================================
@@ -219,61 +226,71 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
@classproperty
def _has_backend_builtin(cls):
- if os.environ.get("PASSLIB_BUILTIN_BCRYPT") != "enabled":
+ if os.environ.get("PASSLIB_BUILTIN_BCRYPT") not in ["enable","enabled"]:
return False
- #look at it cross-eyed, and it loads itself
+ # look at it cross-eyed, and it loads itself
_load_builtin()
return True
@classproperty
def _has_backend_os_crypt(cls):
- # XXX: what to do if only h2 is supported? h1 is very rare.
+ # XXX: what to do if only h2 is supported? h1 is *very* rare.
h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
return test_crypt("test",h1) and test_crypt("test", h2)
@classmethod
def _no_backends_msg(cls):
- return "no BCrypt backends available - please install pybcrypt or bcryptor for BCrypt support"
+ return "no bcrypt backends available - please install 'py-bcrypt' or " \
+ "'bcryptor' for bcrypt support"
def _calc_checksum_os_crypt(self, secret):
- hash = safe_crypt(secret, self.to_string(_for_backend=True))
+ hash = safe_crypt(secret, self._get_config())
if hash:
return hash[-31:]
else:
#NOTE: not checking backends since this is lowest priority,
# so they probably aren't available either
- raise ValueError("encoded password can't be handled by os_crypt"
- " (recommend installing pybcrypt or bcryptor)")
+ raise ValueError("encoded password can't be handled by os_crypt, "
+ "recommend installing py-bcrypt or bcryptor.")
def _calc_checksum_pybcrypt(self, secret):
- #pybcrypt behavior:
+ #py-bcrypt behavior:
# py2: unicode secret/hash encoded as ascii bytes before use,
- # bytes takes as-is; returns ascii bytes.
- # py3: not supported
+ # bytes taken as-is; returns ascii bytes.
+ # py3: not supported (patch submitted)
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- hash = pybcrypt_hashpw(secret, self.to_string(_for_backend=True))
- return hash[-31:].decode("ascii")
+ hash = pybcrypt_hashpw(secret, self._get_config())
+ return str_to_uascii(hash[-31:])
def _calc_checksum_bcryptor(self, secret):
#bcryptor behavior:
# py2: unicode secret/hash encoded as ascii bytes before use,
- # bytes takes as-is; returns ascii bytes.
+ # bytes taken as-is; returns ascii bytes.
# py3: not supported
-
- # FIXME: bcryptor doesn't support v0 hashes ("$2$"),
- # will throw bcryptor.engine.SaltError at this point.
-
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- hash = bcryptor_engine(False).hash_key(secret,
- self.to_string(_for_backend=True))
- return hash[-31:].decode("ascii")
+ if self.ident == IDENT_2:
+ # bcryptor doesn't support $2$ hashes; but we can fake $2$ behavior
+ # using the $2a$ algorithm, by repeating the password until
+ # it's at least 72 chars in length.
+ ss = len(secret)
+ if 0 < ss < 72:
+ secret = secret * (1 + 72//ss)
+ config = self._get_config(IDENT_2A)
+ else:
+ config = self._get_config()
+ hash = bcryptor_engine(False).hash_key(secret, config)
+ return str_to_uascii(hash[-31:])
def _calc_checksum_builtin(self, secret):
if secret is None:
raise TypeError("no secret provided")
+ warn("SECURITY WARNING: Passlib is using it's pure-python bcrypt "
+ "implementation, which is TOO SLOW FOR PRODUCTION USE. It is "
+ "strongly recommended that you install py-bcrypt or bcryptor for "
+ "Passlib to use instead.", PasslibSecurityWarning)
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
chk = _builtin_bcrypt(secret, self.ident.strip("$"),
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index 4547e56..29c77ef 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -119,6 +119,25 @@ class _bcrypt_test(HandlerCase):
'$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
]
+ if enable_option("cover"):
+ #
+ # add some extra tests related to 2/2a
+ #
+ CONFIG_2 = '$2$05$' + '.'*22
+ CONFIG_A = '$2a$05$' + '.'*22
+ known_correct_hashes.extend([
+ ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
+ ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
+ ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
+ ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'),
+ ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
+ ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'),
+ ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
+ ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
+ ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
+ ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
+ ])
+
known_correct_configs = [
('$2a$10$Z17AXnnlpzddNUvnC6cZNO', UPASS_TABLE,
'$2a$10$Z17AXnnlpzddNUvnC6cZNOl54vBeVTewdrxohbPtcwl.GEZFTGjHe'),
@@ -138,7 +157,7 @@ class _bcrypt_test(HandlerCase):
# unsupported (but recognized) minor version
"$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
- # rounds not zero-padded (pybcrypt rejects this, therefore so do we)
+ # rounds not zero-padded (py-bcrypt rejects this, therefore so do we)
'$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'
#NOTE: salts with padding bits set are technically malformed,
@@ -148,6 +167,12 @@ class _bcrypt_test(HandlerCase):
#===============================================================
# override some methods
#===============================================================
+ def setUp(self):
+ HandlerCase.setUp(self)
+ if self.backend == "builtin":
+ warnings.filterwarnings("ignore",
+ "SECURITY WARNING: .*pure-python bcrypt.*")
+
def do_genconfig(self, **kwds):
# override default to speed up tests
kwds.setdefault("rounds", 5)
@@ -169,7 +194,7 @@ class _bcrypt_test(HandlerCase):
def get_fuzz_verifiers(self):
verifiers = super(_bcrypt_test, self).get_fuzz_verifiers()
- # test other backends against pybcrypt if available
+ # test other backends against py-bcrypt if available
from passlib.utils import to_native_str
try:
from bcrypt import hashpw
@@ -184,7 +209,7 @@ class _bcrypt_test(HandlerCase):
try:
return hashpw(secret, hash) == hash
except ValueError:
- raise ValueError("pybcrypt rejected hash: %r" % (hash,))
+ raise ValueError("py-bcrypt rejected hash: %r" % (hash,))
verifiers.append(check_pybcrypt)
# test other backends against bcryptor if available
@@ -198,6 +223,14 @@ class _bcrypt_test(HandlerCase):
secret = to_native_str(secret, self.fuzz_password_encoding)
if hash.startswith("$2y$"):
hash = "$2a$" + hash[4:]
+ elif hash.startswith("$2$"):
+ # bcryptor doesn't support $2$ hashes; but we can fake it
+ # using the $2a$ algorithm, by repeating the password until
+ # it's 72 chars in length.
+ hash = "$2a$" + hash[3:]
+ ss = len(secret)
+ if 0 < ss < 72:
+ secret = secret * (1+72//ss)
return Engine(False).hash_key(secret, hash) == hash
verifiers.append(check_bcryptor)
@@ -212,9 +245,6 @@ class _bcrypt_test(HandlerCase):
if ident == u("$2x$"):
# just recognized, not currently supported.
return None
- if ident == u("$2$") and self.handler.has_backend("bcryptor"):
- # FIXME: skipping this since bcryptor doesn't support v0 hashes
- return None
return ident
#===============================================================
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 6d26d91..24173f7 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -974,11 +974,14 @@ class Base64Engine(object):
if isinstance(source, unicode):
cm = self.charmap
last = cm[cm.index(last) & mask]
+ assert last in padset, "failed to generate valid padding char"
else:
# NOTE: this assumes ascii-compat encoding, and that
# all chars used by encoding are 7-bit ascii.
last = self._encode64(self._decode64(last) & mask)
- assert last in padset, "failed to generate valid padding char"
+ assert last in padset, "failed to generate valid padding char"
+ if PY3:
+ last = bytes([last])
return True, source[:-1] + last
def repair_unused(self, source):