diff options
Diffstat (limited to 'passlib/handlers/bcrypt.py')
-rw-r--r-- | passlib/handlers/bcrypt.py | 197 |
1 files changed, 40 insertions, 157 deletions
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index b83b110..9a10f9e 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -10,7 +10,6 @@ TODO: #============================================================================= # imports #============================================================================= -from __future__ import with_statement, absolute_import # core from base64 import b64encode from hashlib import sha256 @@ -20,8 +19,6 @@ import logging; log = logging.getLogger(__name__) from warnings import warn # site _bcrypt = None # dynamically imported by _load_backend_bcrypt() -_pybcrypt = None # dynamically imported by _load_backend_pybcrypt() -_bcryptor = None # dynamically imported by _load_backend_bcryptor() # pkg _builtin_bcrypt = None # dynamically imported by _load_backend_builtin() from passlib.crypto.digest import compile_hmac @@ -30,8 +27,6 @@ from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \ rng, getrandstr, test_crypt, to_unicode, \ utf8_truncate, utf8_repeat_string, crypt_accepts_bytes from passlib.utils.binary import bcrypt64 -from passlib.utils.compat import get_unbound_method_function -from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii, PY3, error_from import passlib.utils.handlers as uh # local @@ -42,11 +37,11 @@ __all__ = [ #============================================================================= # support funcs & constants #============================================================================= -IDENT_2 = u("$2$") -IDENT_2A = u("$2a$") -IDENT_2X = u("$2x$") -IDENT_2Y = u("$2y$") -IDENT_2B = u("$2b$") +IDENT_2 = u"$2$" +IDENT_2A = u"$2a$" +IDENT_2X = u"$2x$" +IDENT_2Y = u"$2y$" +IDENT_2B = u"$2b$" _BNULL = b'\x00' # reference hash of "test", used in various self-checks @@ -122,8 +117,8 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, #-------------------- default_ident = IDENT_2B ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B) - ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y, - u("2b"): IDENT_2B} + ident_aliases = {u"2": IDENT_2, u"2a": IDENT_2A, u"2y": IDENT_2Y, + u"2b": IDENT_2B} #-------------------- # HasSalt @@ -171,9 +166,9 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, if ident == IDENT_2X: raise ValueError("crypt_blowfish's buggy '2x' hashes are not " "currently supported") - rounds_str, data = tail.split(u("$")) + rounds_str, data = tail.split(u"$") rounds = int(rounds_str) - if rounds_str != u('%02d') % (rounds,): + if rounds_str != u'%02d' % (rounds,): raise uh.exc.MalformedHashError(cls, "malformed cost field") salt, chk = data[:22], data[22:] return cls( @@ -184,15 +179,15 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, ) def to_string(self): - hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum) - return uascii_to_str(hash) + hash = u"%s%02d$%s%s" % (self.ident, self.rounds, self.salt, self.checksum) + return hash # NOTE: this should be kept separate from to_string() # so that bcrypt_sha256() can still use it, while overriding to_string() def _get_config(self, ident): """internal helper to prepare config string for backends""" - config = u("%s%02d$%s") % (ident, self.rounds, self.salt) - return uascii_to_str(config) + config = u"%s%02d$%s" % (ident, self.rounds, self.salt) + return config #=================================================================== # migration @@ -211,7 +206,7 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, # TODO: try to detect incorrect 8bit/wraparound hashes using kwds.get("secret") # hand off to base implementation, so HasRounds can check rounds value. - return super(_BcryptCommon, cls).needs_update(hash, **kwds) + return super().needs_update(hash, **kwds) #=================================================================== # specialized salt generation - fixes passlib issue 25 @@ -229,12 +224,12 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, def _generate_salt(cls): # generate random salt as normal, # but repair last char so the padding bits always decode to zero. - salt = super(_BcryptCommon, cls)._generate_salt() + salt = super()._generate_salt() return bcrypt64.repair_unused(salt) @classmethod def _norm_salt(cls, salt, **kwds): - salt = super(_BcryptCommon, cls)._norm_salt(salt, **kwds) + salt = super()._norm_salt(salt, **kwds) assert salt is not None, "HasSalt didn't generate new salt!" changed, salt = bcrypt64.check_repair_unused(salt) if changed: @@ -248,7 +243,7 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, return salt def _norm_checksum(self, checksum, relaxed=False): - checksum = super(_BcryptCommon, self)._norm_checksum(checksum, relaxed=relaxed) + checksum = super()._norm_checksum(checksum, relaxed=relaxed) changed, checksum = bcrypt64.check_repair_unused(checksum) if changed: warn( @@ -294,8 +289,6 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, verify = mixin_cls.verify err_types = (ValueError, uh.exc.MissingBackendError) - if _bcryptor: - err_types += (_bcryptor.engine.SaltError,) def safe_verify(secret, hash): """verify() wrapper which traps 'unknown identifier' errors""" @@ -309,7 +302,6 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, # - InternalBackendError if crypt fails for unknown reason # (trapped below so we can debug it) # pybcrypt, bcrypt -- raises ValueError - # bcryptor -- raises bcryptor.engine.SaltError return NotImplemented except uh.exc.InternalBackendError: # _calc_checksum() code may also throw CryptBackendError @@ -490,7 +482,7 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, def _norm_digest_args(cls, secret, ident, new=False): # make sure secret is unicode require_valid_utf8_bytes = cls._require_valid_utf8_bytes - if isinstance(secret, unicode): + if isinstance(secret, str): secret = secret.encode("utf-8") elif require_valid_utf8_bytes: # if backend requires utf8 bytes (os_crypt); @@ -510,7 +502,7 @@ class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, cls._check_truncate_policy(secret) # NOTE: especially important to forbid NULLs for bcrypt, since many - # backends (bcryptor, bcrypt) happily accept them, and then + # backends (bcrypt) happily accept them, and then # silently truncate the password at first NULL they encounter! if _BNULL in secret: raise uh.exc.NullPasswordError(cls) @@ -591,7 +583,7 @@ class _NoBackend(_BcryptCommon): self._stub_requires_backend() # NOTE: have to use super() here so that we don't recursively # call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum - return super(bcrypt, self)._calc_checksum(secret) + return super()._calc_checksum(secret) #=================================================================== # eoc @@ -630,7 +622,7 @@ class _BcryptBackend(_BcryptCommon): # # below method has a few edge cases where it chokes though. # @classmethod # def verify(cls, secret, hash): - # if isinstance(hash, unicode): + # if isinstance(hash, str): # hash = hash.encode("ascii") # ident = hash[:hash.index(b"$", 1)+1].decode("ascii") # if ident not in cls.ident_values: @@ -650,7 +642,7 @@ class _BcryptBackend(_BcryptCommon): # returns ascii bytes secret, ident = self._prepare_digest_args(secret) config = self._get_config(ident) - if isinstance(config, unicode): + if isinstance(config, str): config = config.encode("ascii") hash = _bcrypt.hashpw(secret, config) assert isinstance(hash, bytes) @@ -658,113 +650,6 @@ class _BcryptBackend(_BcryptCommon): raise uh.exc.CryptBackendError(self, config, hash, source="`bcrypt` package") return hash[-31:].decode("ascii") -#----------------------------------------------------------------------- -# bcryptor backend -#----------------------------------------------------------------------- -class _BcryptorBackend(_BcryptCommon): - """ - backend which uses 'bcryptor' package - """ - - @classmethod - def _load_backend_mixin(mixin_cls, name, dryrun): - # try to import bcryptor - global _bcryptor - try: - import bcryptor as _bcryptor - except ImportError: # pragma: no cover - return False - - # deprecated as of 1.7.2 - if not dryrun: - warn("Support for `bcryptor` is deprecated, and will be removed in Passlib 1.8; " - "Please use `pip install bcrypt` instead", DeprecationWarning) - - return mixin_cls._finalize_backend_mixin(name, dryrun) - - def _calc_checksum(self, secret): - # bcryptor behavior: - # py2: unicode secret/hash encoded as ascii bytes before use, - # bytes taken as-is; returns ascii bytes. - # py3: not supported - secret, ident = self._prepare_digest_args(secret) - config = self._get_config(ident) - hash = _bcryptor.engine.Engine(False).hash_key(secret, config) - if not hash.startswith(config) or len(hash) != len(config) + 31: - raise uh.exc.CryptBackendError(self, config, hash, source="bcryptor library") - return str_to_uascii(hash[-31:]) - -#----------------------------------------------------------------------- -# pybcrypt backend -#----------------------------------------------------------------------- -class _PyBcryptBackend(_BcryptCommon): - """ - backend which uses 'pybcrypt' package - """ - - #: classwide thread lock used for pybcrypt < 0.3 - _calc_lock = None - - @classmethod - def _load_backend_mixin(mixin_cls, name, dryrun): - # try to import pybcrypt - global _pybcrypt - if not _detect_pybcrypt(): - # not installed, or bcrypt installed instead - return False - try: - import bcrypt as _pybcrypt - except ImportError: # pragma: no cover - # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken) - return False - - # deprecated as of 1.7.2 - if not dryrun: - warn("Support for `py-bcrypt` is deprecated, and will be removed in Passlib 1.8; " - "Please use `pip install bcrypt` instead", DeprecationWarning) - - # determine pybcrypt version - try: - version = _pybcrypt._bcrypt.__version__ - except: - log.warning("(trapped) error reading pybcrypt version", exc_info=True) - version = "<unknown>" - log.debug("detected 'pybcrypt' backend, version %r", version) - - # return calc function based on version - vinfo = parse_version(version) or (0, 0) - if vinfo < (0, 3): - warn("py-bcrypt %s has a major security vulnerability, " - "you should upgrade to py-bcrypt 0.3 immediately." - % version, uh.exc.PasslibSecurityWarning) - if mixin_cls._calc_lock is None: - import threading - mixin_cls._calc_lock = threading.Lock() - mixin_cls._calc_checksum = get_unbound_method_function(mixin_cls._calc_checksum_threadsafe) - - return mixin_cls._finalize_backend_mixin(name, dryrun) - - def _calc_checksum_threadsafe(self, secret): - # as workaround for pybcrypt < 0.3's concurrency issue, - # we wrap everything in a thread lock. as long as bcrypt is only - # used through passlib, this should be safe. - with self._calc_lock: - return self._calc_checksum_raw(secret) - - def _calc_checksum_raw(self, secret): - # py-bcrypt behavior: - # py2: unicode secret/hash encoded as ascii bytes before use, - # bytes taken as-is; returns ascii bytes. - # py3: unicode secret encoded as utf-8 bytes, - # hash encoded as ascii bytes, returns ascii unicode. - secret, ident = self._prepare_digest_args(secret) - config = self._get_config(ident) - hash = _pybcrypt.hashpw(secret, config) - if not hash.startswith(config) or len(hash) != len(config) + 31: - raise uh.exc.CryptBackendError(self, config, hash, source="pybcrypt library") - return str_to_uascii(hash[-31:]) - - _calc_checksum = _calc_checksum_raw #----------------------------------------------------------------------- # os crypt backend @@ -810,14 +695,14 @@ class _OsCryptBackend(_BcryptCommon): # instead of returning None? (would save re-detecting what went wrong) # XXX: isn't secret ALWAYS bytes at this point? # - if PY3 and isinstance(secret, bytes): + if isinstance(secret, bytes): try: secret.decode("utf-8") except UnicodeDecodeError: - raise error_from(uh.exc.PasswordValueError( + raise uh.exc.PasswordValueError( "python3 crypt.crypt() ony supports bytes passwords using UTF8; " "passlib recommends running `pip install bcrypt` for general bcrypt support.", - ), None) + ) from None # # else crypt() call failed for unknown reason. @@ -943,7 +828,7 @@ class bcrypt(_NoBackend, _BcryptCommon): # in order to load the appropriate backend. #: list of potential backends - backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin") + backends = ("bcrypt", "os_crypt", "builtin") #: flag that this class's bases should be modified by SubclassBackendMixin _backend_mixin_target = True @@ -952,8 +837,6 @@ class bcrypt(_NoBackend, _BcryptCommon): _backend_mixin_map = { None: _NoBackend, "bcrypt": _BcryptBackend, - "pybcrypt": _PyBcryptBackend, - "bcryptor": _BcryptorBackend, "os_crypt": _OsCryptBackend, "builtin": _BuiltinBackend, } @@ -965,7 +848,7 @@ class bcrypt(_NoBackend, _BcryptCommon): #============================================================================= # variants #============================================================================= -_UDOLLAR = u("$") +_UDOLLAR = u"$" # XXX: it might be better to have all the bcrypt variants share a common base class, # and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it. @@ -983,17 +866,17 @@ class _wrapped_bcrypt(bcrypt): # def hash(cls, secret, **kwds): # # bypass bcrypt backend overriding this method # # XXX: would wrapping bcrypt make this easier than subclassing it? - # return super(_BcryptCommon, cls).hash(secret, **kwds) + # return super().hash(secret, **kwds) # # @classmethod # def verify(cls, secret, hash): # # bypass bcrypt backend overriding this method - # return super(_BcryptCommon, cls).verify(secret, hash) + # return super().verify(secret, hash) # # @classmethod # def genhash(cls, secret, hash): # # bypass bcrypt backend overriding this method - # return super(_BcryptCommon, cls).genhash(secret, hash) + # return super().genhash(secret, hash) @classmethod def _check_truncate_policy(cls, secret): @@ -1050,7 +933,7 @@ class bcrypt_sha256(_wrapped_bcrypt): # class specific #-------------------- - _supported_versions = set([1, 2]) + _supported_versions = {1, 2} #=================================================================== # instance attrs @@ -1067,7 +950,7 @@ class bcrypt_sha256(_wrapped_bcrypt): @classmethod def using(cls, version=None, **kwds): - subcls = super(bcrypt_sha256, cls).using(**kwds) + subcls = super().using(**kwds) if version is not None: subcls.version = subcls._norm_version(version) ident = subcls.default_ident @@ -1093,7 +976,7 @@ class bcrypt_sha256(_wrapped_bcrypt): # XXX: we can't use .ident attr due to bcrypt code using it. # working around that via prefix. - prefix = u('$bcrypt-sha256$') + prefix = u'$bcrypt-sha256$' #: current version 2 hash format _v2_hash_re = re.compile(r"""(?x) @@ -1152,8 +1035,8 @@ class bcrypt_sha256(_wrapped_bcrypt): checksum=m.group("digest"), ) - _v2_template = u("$bcrypt-sha256$v=2,t=%s,r=%d$%s$%s") - _v1_template = u("$bcrypt-sha256$%s,%d$%s$%s") + _v2_template = u"$bcrypt-sha256$v=2,t=%s,r=%d$%s$%s" + _v1_template = u"$bcrypt-sha256$%s,%d$%s$%s" def to_string(self): if self.version == 1: @@ -1161,7 +1044,7 @@ class bcrypt_sha256(_wrapped_bcrypt): else: template = self._v2_template hash = template % (self.ident.strip(_UDOLLAR), self.rounds, self.salt, self.checksum) - return uascii_to_str(hash) + return hash #=================================================================== # init @@ -1170,7 +1053,7 @@ class bcrypt_sha256(_wrapped_bcrypt): def __init__(self, version=None, **kwds): if version is not None: self.version = self._norm_version(version) - super(bcrypt_sha256, self).__init__(**kwds) + super().__init__(**kwds) #=================================================================== # version @@ -1193,7 +1076,7 @@ class bcrypt_sha256(_wrapped_bcrypt): # thus, have to use base64 (44 bytes) rather than hex (64 bytes). # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha256 # may switch to hex encoding, since it's simpler to implement elsewhere. - if isinstance(secret, unicode): + if isinstance(secret, str): secret = secret.encode("utf-8") if self.version == 1: @@ -1223,7 +1106,7 @@ class bcrypt_sha256(_wrapped_bcrypt): key = b64encode(digest) # hand result off to normal bcrypt algorithm - return super(bcrypt_sha256, self)._calc_checksum(key) + return super()._calc_checksum(key) #=================================================================== # other @@ -1232,7 +1115,7 @@ class bcrypt_sha256(_wrapped_bcrypt): def _calc_needs_update(self, **kwds): if self.version < type(self).version: return True - return super(bcrypt_sha256, self)._calc_needs_update(**kwds) + return super()._calc_needs_update(**kwds) #=================================================================== # eoc |