Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
bcrypt.py1244 linesDownload Raw Back to handlers
1"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm.2 3TODO:4 5* support 2x and altered-2a hashes?6  http://www.openwall.com/lists/oss-security/2011/06/27/97 8* deal with lack of PY3-compatibile c-ext implementation9"""10#=============================================================================11# imports12#=============================================================================13from __future__ import with_statement, absolute_import14# core15from base64 import b64encode16from hashlib import sha25617import os18import re19import logging; log = logging.getLogger(__name__)20from warnings import warn21# site22_bcrypt = None # dynamically imported by _load_backend_bcrypt()23_pybcrypt = None # dynamically imported by _load_backend_pybcrypt()24_bcryptor = None # dynamically imported by _load_backend_bcryptor()25# pkg26_builtin_bcrypt = None  # dynamically imported by _load_backend_builtin()27from passlib.crypto.digest import compile_hmac28from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError29from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \30                          rng, getrandstr, test_crypt, to_unicode, \31                          utf8_truncate, utf8_repeat_string, crypt_accepts_bytes32from passlib.utils.binary import bcrypt6433from passlib.utils.compat import get_unbound_method_function34from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii, PY3, error_from35import passlib.utils.handlers as uh36 37# local38__all__ = [39    "bcrypt",40]41 42#=============================================================================43# support funcs & constants44#=============================================================================45IDENT_2 = u("$2$")46IDENT_2A = u("$2a$")47IDENT_2X = u("$2x$")48IDENT_2Y = u("$2y$")49IDENT_2B = u("$2b$")50_BNULL = b'\x00'51 52# reference hash of "test", used in various self-checks53TEST_HASH_2A = "$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK"54 55def _detect_pybcrypt():56    """57    internal helper which tries to distinguish pybcrypt vs bcrypt.58 59    :returns:60        True if cext-based py-bcrypt,61        False if ffi-based bcrypt,62        None if 'bcrypt' module not found.63 64    .. versionchanged:: 1.6.365 66        Now assuming bcrypt installed, unless py-bcrypt explicitly detected.67        Previous releases assumed py-bcrypt by default.68 69        Making this change since py-bcrypt is (apparently) unmaintained and static,70        whereas bcrypt is being actively maintained, and it's internal structure may shift.71    """72    # NOTE: this is also used by the unittests.73 74    # check for module.75    try:76        import bcrypt77    except ImportError:78        # XXX: this is ignoring case where py-bcrypt's "bcrypt._bcrypt" C Ext fails to import;79        #      would need to inspect actual ImportError message to catch that.80        return None81 82    # py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4),83    # which bcrypt lacks (confirmed for v1.0 - 2.0)84    # "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute.85    try:86        from bcrypt._bcrypt import __version__87    except ImportError:88        return False89    return True90 91#=============================================================================92# backend mixins93#=============================================================================94class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents,95                    uh.HasRounds, uh.HasSalt, uh.GenericHandler):96    """97    Base class which implements brunt of BCrypt code.98    This is then subclassed by the various backends,99    to override w/ backend-specific methods.100 101    When a backend is loaded, the bases of the 'bcrypt' class proper102    are modified to prepend the correct backend-specific subclass.103    """104    #===================================================================105    # class attrs106    #===================================================================107 108    #--------------------109    # PasswordHash110    #--------------------111    name = "bcrypt"112    setting_kwds = ("salt", "rounds", "ident", "truncate_error")113 114    #--------------------115    # GenericHandler116    #--------------------117    checksum_size = 31118    checksum_chars = bcrypt64.charmap119 120    #--------------------121    # HasManyIdents122    #--------------------123    default_ident = IDENT_2B124    ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B)125    ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A,  u("2y"): IDENT_2Y,126                     u("2b"): IDENT_2B}127 128    #--------------------129    # HasSalt130    #--------------------131    min_salt_size = max_salt_size = 22132    salt_chars = bcrypt64.charmap133 134    # NOTE: 22nd salt char must be in restricted set of ``final_salt_chars``, not full set above.135    final_salt_chars = ".Oeu"  # bcrypt64._padinfo2[1]136 137    #--------------------138    # HasRounds139    #--------------------140    default_rounds = 12 # current passlib default141    min_rounds = 4 # minimum from bcrypt specification142    max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)143    rounds_cost = "log2"144 145    #--------------------146    # TruncateMixin147    #--------------------148    truncate_size = 72149 150    #--------------------151    # custom152    #--------------------153 154    # backend workaround detection flags155    # NOTE: these are only set on the backend mixin classes156    _workrounds_initialized = False157    _has_2a_wraparound_bug = False158    _lacks_20_support = False159    _lacks_2y_support = False160    _lacks_2b_support = False161    _fallback_ident = IDENT_2A162    _require_valid_utf8_bytes = False163 164    #===================================================================165    # formatting166    #===================================================================167 168    @classmethod169    def from_string(cls, hash):170        ident, tail = cls._parse_ident(hash)171        if ident == IDENT_2X:172            raise ValueError("crypt_blowfish's buggy '2x' hashes are not "173                             "currently supported")174        rounds_str, data = tail.split(u("$"))175        rounds = int(rounds_str)176        if rounds_str != u('%02d') % (rounds,):177            raise uh.exc.MalformedHashError(cls, "malformed cost field")178        salt, chk = data[:22], data[22:]179        return cls(180            rounds=rounds,181            salt=salt,182            checksum=chk or None,183            ident=ident,184        )185 186    def to_string(self):187        hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum)188        return uascii_to_str(hash)189 190    # NOTE: this should be kept separate from to_string()191    #       so that bcrypt_sha256() can still use it, while overriding to_string()192    def _get_config(self, ident):193        """internal helper to prepare config string for backends"""194        config = u("%s%02d$%s") % (ident, self.rounds, self.salt)195        return uascii_to_str(config)196 197    #===================================================================198    # migration199    #===================================================================200 201    @classmethod202    def needs_update(cls, hash, **kwds):203        # NOTE: can't convert this to use _calc_needs_update() helper,204        #       since _norm_hash() will correct salt padding before we can read it here.205        # check for incorrect padding bits (passlib issue 25)206        if isinstance(hash, bytes):207            hash = hash.decode("ascii")208        if hash.startswith(IDENT_2A) and hash[28] not in cls.final_salt_chars:209            return True210 211        # TODO: try to detect incorrect 8bit/wraparound hashes using kwds.get("secret")212 213        # hand off to base implementation, so HasRounds can check rounds value.214        return super(_BcryptCommon, cls).needs_update(hash, **kwds)215 216    #===================================================================217    # specialized salt generation - fixes passlib issue 25218    #===================================================================219 220    @classmethod221    def normhash(cls, hash):222        """helper to normalize hash, correcting any bcrypt padding bits"""223        if cls.identify(hash):224            return cls.from_string(hash).to_string()225        else:226            return hash227 228    @classmethod229    def _generate_salt(cls):230        # generate random salt as normal,231        # but repair last char so the padding bits always decode to zero.232        salt = super(_BcryptCommon, cls)._generate_salt()233        return bcrypt64.repair_unused(salt)234 235    @classmethod236    def _norm_salt(cls, salt, **kwds):237        salt = super(_BcryptCommon, cls)._norm_salt(salt, **kwds)238        assert salt is not None, "HasSalt didn't generate new salt!"239        changed, salt = bcrypt64.check_repair_unused(salt)240        if changed:241            # FIXME: if salt was provided by user, this message won't be242            # correct. not sure if we want to throw error, or use different warning.243            warn(244                "encountered a bcrypt salt with incorrectly set padding bits; "245                "you may want to use bcrypt.normhash() "246                "to fix this; this will be an error under Passlib 2.0",247                PasslibHashWarning)248        return salt249 250    def _norm_checksum(self, checksum, relaxed=False):251        checksum = super(_BcryptCommon, self)._norm_checksum(checksum, relaxed=relaxed)252        changed, checksum = bcrypt64.check_repair_unused(checksum)253        if changed:254            warn(255                "encountered a bcrypt hash with incorrectly set padding bits; "256                "you may want to use bcrypt.normhash() "257                "to fix this; this will be an error under Passlib 2.0",258                PasslibHashWarning)259        return checksum260 261    #===================================================================262    # backend configuration263    # NOTE: backends are defined in terms of mixin classes,264    #       which are dynamically inserted into the bases of the 'bcrypt' class265    #       via the machinery in 'SubclassBackendMixin'.266    #       this lets us load in a backend-specific implementation267    #       of _calc_checksum() and similar methods.268    #===================================================================269 270    # NOTE: backend config is located down in <bcrypt> class271 272    # NOTE: set_backend() will execute the ._load_backend_mixin()273    #       of the matching mixin class, which will handle backend detection274 275    # appended to HasManyBackends' "no backends available" error message276    _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')"277 278    @classmethod279    def _finalize_backend_mixin(mixin_cls, backend, dryrun):280        """281        helper called by from backend mixin classes' _load_backend_mixin() --282        invoked after backend imports have been loaded, and performs283        feature detection & testing common to all backends.284        """285        #----------------------------------------------------------------286        # setup helpers287        #----------------------------------------------------------------288        assert mixin_cls is bcrypt._backend_mixin_map[backend], \289            "_configure_workarounds() invoked from wrong class"290 291        if mixin_cls._workrounds_initialized:292            return True293 294        verify = mixin_cls.verify295 296        err_types = (ValueError, uh.exc.MissingBackendError)297        if _bcryptor:298            err_types += (_bcryptor.engine.SaltError,)299 300        def safe_verify(secret, hash):301            """verify() wrapper which traps 'unknown identifier' errors"""302            try:303                return verify(secret, hash)304            except err_types:305                # backends without support for given ident will throw various306                # errors about unrecognized version:307                #   os_crypt -- internal code below throws308                #       - PasswordValueError if there's encoding issue w/ password.309                #       - InternalBackendError if crypt fails for unknown reason310                #         (trapped below so we can debug it)311                #   pybcrypt, bcrypt -- raises ValueError312                #   bcryptor -- raises bcryptor.engine.SaltError313                return NotImplemented314            except uh.exc.InternalBackendError:315                # _calc_checksum() code may also throw CryptBackendError316                # if correct hash isn't returned (e.g. 2y hash converted to 2b,317                # such as happens with bcrypt 3.0.0)318                log.debug("trapped unexpected response from %r backend: verify(%r, %r):",319                          backend, secret, hash, exc_info=True)320                return NotImplemented321 322        def assert_lacks_8bit_bug(ident):323            """324            helper to check for cryptblowfish 8bit bug (fixed in 2y/2b);325            even though it's not known to be present in any of passlib's backends.326            this is treated as FATAL, because it can easily result in seriously malformed hashes,327            and we can't correct for it ourselves.328 329            test cases from <http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/wrapper.c.diff?r1=1.9;r2=1.10>330            reference hash is the incorrectly generated $2x$ hash taken from above url331            """332            # NOTE: passlib 1.7.2 and earlier used the commented-out LATIN-1 test vector to detect333            #       this bug; but python3's crypt.crypt() only supports unicode inputs (and334            #       always encodes them as UTF8 before passing to crypt); so passlib 1.7.3335            #       switched to the UTF8-compatible test vector below.  This one's bug_hash value336            #       ("$2x$...rcAS") was drawn from the same openwall source (above); and the correct337            #       hash ("$2a$...X6eu") was generated by passing the raw bytes to python2's338            #       crypt.crypt() using OpenBSD 6.7 (hash confirmed as same for $2a$ & $2b$).339 340            # LATIN-1 test vector341            # secret = b"\xA3"342            # bug_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"343            # correct_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq"344 345            # UTF-8 test vector346            secret = b"\xd1\x91"  # aka "\u0451"347            bug_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OiwqTymGIGzFsA4hOTWebfehXHNprcAS"348            correct_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu"349 350            if verify(secret, bug_hash):351                # NOTE: this only EVER be observed in (broken) 2a and (backward-compat) 2x hashes352                #       generated by crypt_blowfish library. 2y/2b hashes should not have the bug353                #       (but we check w/ them anyways).354                raise PasslibSecurityError(355                    "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "356                    "the crypt_blowfish 8-bit bug (CVE-2011-2483) under %r hashes, "357                    "and should be upgraded or replaced with another backend" % (backend, ident))358 359            # it doesn't have wraparound bug, but make sure it *does* verify against the correct360            # hash, or we're in some weird third case!361            if not verify(secret, correct_hash):362                raise RuntimeError("%s backend failed to verify %s 8bit hash" % (backend, ident))363 364        def detect_wrap_bug(ident):365            """366            check for bsd wraparound bug (fixed in 2b)367            this is treated as a warning, because it's rare in the field,368            and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it.369 370            test cases from <http://www.openwall.com/lists/oss-security/2012/01/02/4>371 372            NOTE: reference hash is of password "0"*72373 374            NOTE: if in future we need to deliberately create hashes which have this bug,375                  can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)'376            """377            # check if it exhibits wraparound bug378            secret = (b"0123456789"*26)[:255]379            bug_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"380            if verify(secret, bug_hash):381                return True382 383            # if it doesn't have wraparound bug, make sure it *does* handle things384            # correctly -- or we're in some weird third case.385            correct_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi"386            if not verify(secret, correct_hash):387                raise RuntimeError("%s backend failed to verify %s wraparound hash" % (backend, ident))388 389            return False390 391        def assert_lacks_wrap_bug(ident):392            if not detect_wrap_bug(ident):393                return394            # should only see in 2a, later idents should NEVER exhibit this bug:395            # * 2y implementations should have been free of it396            # * 2b was what (supposedly) fixed it397            raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident))398 399        #----------------------------------------------------------------400        # check for old 20 support401        #----------------------------------------------------------------402        test_hash_20 = b"$2$04$5BJqKfqMQvV7nS.yUguNcuRfMMOXK0xPWavM7pOzjEi5ze5T1k8/S"403        result = safe_verify("test", test_hash_20)404        if result is NotImplemented:405            mixin_cls._lacks_20_support = True406            log.debug("%r backend lacks $2$ support, enabling workaround", backend)407        elif not result:408            raise RuntimeError("%s incorrectly rejected $2$ hash" % backend)409 410        #----------------------------------------------------------------411        # check for 2a support412        #----------------------------------------------------------------413        result = safe_verify("test", TEST_HASH_2A)414        if result is NotImplemented:415            # 2a support is required, and should always be present416            raise RuntimeError("%s lacks support for $2a$ hashes" % backend)417        elif not result:418            raise RuntimeError("%s incorrectly rejected $2a$ hash" % backend)419        else:420            assert_lacks_8bit_bug(IDENT_2A)421            if detect_wrap_bug(IDENT_2A):422                if backend == "os_crypt":423                    # don't make this a warning for os crypt (e.g. openbsd);424                    # they'll have proper 2b implementation which will be used for new hashes.425                    # so even if we didn't have a workaround, this bug wouldn't be a concern.426                    log.debug("%r backend has $2a$ bsd wraparound bug, enabling workaround", backend)427                else:428                    # installed library has the bug -- want to let users know,429                    # so they can upgrade it to something better (e.g. bcrypt cffi library)430                    warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "431                         "the bsd wraparound bug, "432                         "and should be upgraded or replaced with another backend "433                         "(enabling workaround for now)." % backend,434                         uh.exc.PasslibSecurityWarning)435                mixin_cls._has_2a_wraparound_bug = True436 437        #----------------------------------------------------------------438        # check for 2y support439        #----------------------------------------------------------------440        test_hash_2y = TEST_HASH_2A.replace("2a", "2y")441        result = safe_verify("test", test_hash_2y)442        if result is NotImplemented:443            mixin_cls._lacks_2y_support = True444            log.debug("%r backend lacks $2y$ support, enabling workaround", backend)445        elif not result:446            raise RuntimeError("%s incorrectly rejected $2y$ hash" % backend)447        else:448            # NOTE: Not using this as fallback candidate,449            #       lacks wide enough support across implementations.450            assert_lacks_8bit_bug(IDENT_2Y)451            assert_lacks_wrap_bug(IDENT_2Y)452 453        #----------------------------------------------------------------454        # TODO: check for 2x support455        #----------------------------------------------------------------456 457        #----------------------------------------------------------------458        # check for 2b support459        #----------------------------------------------------------------460        test_hash_2b = TEST_HASH_2A.replace("2a", "2b")461        result = safe_verify("test", test_hash_2b)462        if result is NotImplemented:463            mixin_cls._lacks_2b_support = True464            log.debug("%r backend lacks $2b$ support, enabling workaround", backend)465        elif not result:466            raise RuntimeError("%s incorrectly rejected $2b$ hash" % backend)467        else:468            mixin_cls._fallback_ident = IDENT_2B469            assert_lacks_8bit_bug(IDENT_2B)470            assert_lacks_wrap_bug(IDENT_2B)471 472        # set flag so we don't have to run this again473        mixin_cls._workrounds_initialized = True474        return True475 476    #===================================================================477    # digest calculation478    #===================================================================479 480    # _calc_checksum() defined by backends481 482    def _prepare_digest_args(self, secret):483        """484        common helper for backends to implement _calc_checksum().485        takes in secret, returns (secret, ident) pair,486        """487        return self._norm_digest_args(secret, self.ident, new=self.use_defaults)488 489    @classmethod490    def _norm_digest_args(cls, secret, ident, new=False):491        # make sure secret is unicode492        require_valid_utf8_bytes = cls._require_valid_utf8_bytes493        if isinstance(secret, unicode):494            secret = secret.encode("utf-8")495        elif require_valid_utf8_bytes:496            # if backend requires utf8 bytes (os_crypt);497            # make sure input actually is utf8, or don't bother enabling utf-8 specific helpers.498            try:499                secret.decode("utf-8")500            except UnicodeDecodeError:501                # XXX: could just throw PasswordValueError here, backend will just do that502                #      when _calc_digest() is actually called.503                require_valid_utf8_bytes = False504 505        # check max secret size506        uh.validate_secret(secret)507 508        # check for truncation (during .hash() calls only)509        if new:510            cls._check_truncate_policy(secret)511 512        # NOTE: especially important to forbid NULLs for bcrypt, since many513        # backends (bcryptor, bcrypt) happily accept them, and then514        # silently truncate the password at first NULL they encounter!515        if _BNULL in secret:516            raise uh.exc.NullPasswordError(cls)517 518        # TODO: figure out way to skip these tests when not needed...519 520        # protect from wraparound bug by truncating secret before handing it to the backend.521        # bcrypt only uses first 72 bytes anyways.522        # NOTE: not needed for 2y/2b, but might use 2a as fallback for them.523        if cls._has_2a_wraparound_bug and len(secret) >= 255:524            if require_valid_utf8_bytes:525                # backend requires valid utf8 bytes, so truncate secret to nearest valid segment.526                # want to do this in constant time to not give away info about secret.527                # NOTE: this only works because bcrypt will ignore everything past528                #       secret[71], so padding to include a full utf8 sequence529                #       won't break anything about the final output.530                secret = utf8_truncate(secret, 72)531            else:532                secret = secret[:72]533 534        # special case handling for variants (ordered most common first)535        if ident == IDENT_2A:536            # nothing needs to be done.537            pass538 539        elif ident == IDENT_2B:540            if cls._lacks_2b_support:541                # handle $2b$ hash format even if backend is too old.542                # have it generate a 2A/2Y digest, then return it as a 2B hash.543                # 2a-only backend could potentially exhibit wraparound bug --544                # but we work around that issue above.545                ident = cls._fallback_ident546 547        elif ident == IDENT_2Y:548            if cls._lacks_2y_support:549                # handle $2y$ hash format (not supported by BSDs, being phased out on others)550                # have it generate a 2A/2B digest, then return it as a 2Y hash.551                ident = cls._fallback_ident552 553        elif ident == IDENT_2:554            if cls._lacks_20_support:555                # handle legacy $2$ format (not supported by most backends except BSD os_crypt)556                # we can fake $2$ behavior using the 2A/2Y/2B algorithm557                # by repeating the password until it's at least 72 chars in length.558                if secret:559                    if require_valid_utf8_bytes:560                        # NOTE: this only works because bcrypt will ignore everything past561                        #       secret[71], so padding to include a full utf8 sequence562                        #       won't break anything about the final output.563                        secret = utf8_repeat_string(secret, 72)564                    else:565                        secret = repeat_string(secret, 72)566                ident = cls._fallback_ident567 568        elif ident == IDENT_2X:569 570            # NOTE: shouldn't get here.571            # XXX: could check if backend does actually offer 'support'572            raise RuntimeError("$2x$ hashes not currently supported by passlib")573 574        else:575            raise AssertionError("unexpected ident value: %r" % ident)576 577        return secret, ident578 579#-----------------------------------------------------------------------580# stub backend581#-----------------------------------------------------------------------582class _NoBackend(_BcryptCommon):583    """584    mixin used before any backend has been loaded.585    contains stubs that force loading of one of the available backends.586    """587    #===================================================================588    # digest calculation589    #===================================================================590    def _calc_checksum(self, secret):591        self._stub_requires_backend()592        # NOTE: have to use super() here so that we don't recursively593        #       call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum594        return super(bcrypt, self)._calc_checksum(secret)595 596    #===================================================================597    # eoc598    #===================================================================599 600#-----------------------------------------------------------------------601# bcrypt backend602#-----------------------------------------------------------------------603class _BcryptBackend(_BcryptCommon):604    """605    backend which uses 'bcrypt' package606    """607 608    @classmethod609    def _load_backend_mixin(mixin_cls, name, dryrun):610        # try to import bcrypt611        global _bcrypt612        if _detect_pybcrypt():613            # pybcrypt was installed instead614            return False615        try:616            import bcrypt as _bcrypt617        except ImportError: # pragma: no cover618            return False619        try:620            version = _bcrypt.__about__.__version__621        except:622            log.warning("(trapped) error reading bcrypt version", exc_info=True)623            version = '<unknown>'624 625        log.debug("detected 'bcrypt' backend, version %r", version)626        return mixin_cls._finalize_backend_mixin(name, dryrun)627 628    # # TODO: would like to implementing verify() directly,629    # #       to skip need for parsing hash strings.630    # #       below method has a few edge cases where it chokes though.631    # @classmethod632    # def verify(cls, secret, hash):633    #     if isinstance(hash, unicode):634    #         hash = hash.encode("ascii")635    #     ident = hash[:hash.index(b"$", 1)+1].decode("ascii")636    #     if ident not in cls.ident_values:637    #         raise uh.exc.InvalidHashError(cls)638    #     secret, eff_ident = cls._norm_digest_args(secret, ident)639    #     if eff_ident != ident:640    #         # lacks support for original ident, replace w/ new one.641    #         hash = eff_ident.encode("ascii") + hash[len(ident):]642    #     result = _bcrypt.hashpw(secret, hash)643    #     assert result.startswith(eff_ident)644    #     return consteq(result, hash)645 646    def _calc_checksum(self, secret):647        # bcrypt behavior:648        #   secret must be bytes649        #   config must be ascii bytes650        #   returns ascii bytes651        secret, ident = self._prepare_digest_args(secret)652        config = self._get_config(ident)653        if isinstance(config, unicode):654            config = config.encode("ascii")655        hash = _bcrypt.hashpw(secret, config)656        assert isinstance(hash, bytes)657        if not hash.startswith(config) or len(hash) != len(config)+31:658            raise uh.exc.CryptBackendError(self, config, hash, source="`bcrypt` package")659        return hash[-31:].decode("ascii")660 661#-----------------------------------------------------------------------662# bcryptor backend663#-----------------------------------------------------------------------664class _BcryptorBackend(_BcryptCommon):665    """666    backend which uses 'bcryptor' package667    """668 669    @classmethod670    def _load_backend_mixin(mixin_cls, name, dryrun):671        # try to import bcryptor672        global _bcryptor673        try:674            import bcryptor as _bcryptor675        except ImportError: # pragma: no cover676            return False677 678        # deprecated as of 1.7.2679        if not dryrun:680            warn("Support for `bcryptor` is deprecated, and will be removed in Passlib 1.8; "681                 "Please use `pip install bcrypt` instead", DeprecationWarning)682 683        return mixin_cls._finalize_backend_mixin(name, dryrun)684 685    def _calc_checksum(self, secret):686        # bcryptor behavior:687        #   py2: unicode secret/hash encoded as ascii bytes before use,688        #        bytes taken as-is; returns ascii bytes.689        #   py3: not supported690        secret, ident = self._prepare_digest_args(secret)691        config = self._get_config(ident)692        hash = _bcryptor.engine.Engine(False).hash_key(secret, config)693        if not hash.startswith(config) or len(hash) != len(config) + 31:694            raise uh.exc.CryptBackendError(self, config, hash, source="bcryptor library")695        return str_to_uascii(hash[-31:])696 697#-----------------------------------------------------------------------698# pybcrypt backend699#-----------------------------------------------------------------------700class _PyBcryptBackend(_BcryptCommon):701    """702    backend which uses 'pybcrypt' package703    """704 705    #: classwide thread lock used for pybcrypt < 0.3706    _calc_lock = None707 708    @classmethod709    def _load_backend_mixin(mixin_cls, name, dryrun):710        # try to import pybcrypt711        global _pybcrypt712        if not _detect_pybcrypt():713            # not installed, or bcrypt installed instead714            return False715        try:716            import bcrypt as _pybcrypt717        except ImportError: # pragma: no cover718            # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken)719            return False720 721        # deprecated as of 1.7.2722        if not dryrun:723            warn("Support for `py-bcrypt` is deprecated, and will be removed in Passlib 1.8; "724                 "Please use `pip install bcrypt` instead", DeprecationWarning)725 726        # determine pybcrypt version727        try:728            version = _pybcrypt._bcrypt.__version__729        except:730            log.warning("(trapped) error reading pybcrypt version", exc_info=True)731            version = "<unknown>"732        log.debug("detected 'pybcrypt' backend, version %r", version)733 734        # return calc function based on version735        vinfo = parse_version(version) or (0, 0)736        if vinfo < (0, 3):737            warn("py-bcrypt %s has a major security vulnerability, "738                 "you should upgrade to py-bcrypt 0.3 immediately."739                 % version, uh.exc.PasslibSecurityWarning)740            if mixin_cls._calc_lock is None:741                import threading742                mixin_cls._calc_lock = threading.Lock()743            mixin_cls._calc_checksum = get_unbound_method_function(mixin_cls._calc_checksum_threadsafe)744 745        return mixin_cls._finalize_backend_mixin(name, dryrun)746 747    def _calc_checksum_threadsafe(self, secret):748        # as workaround for pybcrypt < 0.3's concurrency issue,749        # we wrap everything in a thread lock. as long as bcrypt is only750        # used through passlib, this should be safe.751        with self._calc_lock:752            return self._calc_checksum_raw(secret)753 754    def _calc_checksum_raw(self, secret):755        # py-bcrypt behavior:756        #   py2: unicode secret/hash encoded as ascii bytes before use,757        #        bytes taken as-is; returns ascii bytes.758        #   py3: unicode secret encoded as utf-8 bytes,759        #        hash encoded as ascii bytes, returns ascii unicode.760        secret, ident = self._prepare_digest_args(secret)761        config = self._get_config(ident)762        hash = _pybcrypt.hashpw(secret, config)763        if not hash.startswith(config) or len(hash) != len(config) + 31:764            raise uh.exc.CryptBackendError(self, config, hash, source="pybcrypt library")765        return str_to_uascii(hash[-31:])766 767    _calc_checksum = _calc_checksum_raw768 769#-----------------------------------------------------------------------770# os crypt backend771#-----------------------------------------------------------------------772class _OsCryptBackend(_BcryptCommon):773    """774    backend which uses :func:`crypt.crypt`775    """776 777    #: set flag to ensure _prepare_digest_args() doesn't create invalid utf8 string778    #: when truncating bytes.779    _require_valid_utf8_bytes = not crypt_accepts_bytes780 781    @classmethod782    def _load_backend_mixin(mixin_cls, name, dryrun):783        if not test_crypt("test", TEST_HASH_2A):784            return False785        return mixin_cls._finalize_backend_mixin(name, dryrun)786 787    def _calc_checksum(self, secret):788        #789        # run secret through crypt.crypt().790        # if everything goes right, we'll get back a properly formed bcrypt hash.791        #792        secret, ident = self._prepare_digest_args(secret)793        config = self._get_config(ident)794        hash = safe_crypt(secret, config)795        if hash is not None:796            if not hash.startswith(config) or len(hash) != len(config) + 31:797                raise uh.exc.CryptBackendError(self, config, hash)798            return hash[-31:]799 800        #801        # Check if this failed due to non-UTF8 bytes802        # In detail: under py3, crypt.crypt() requires unicode inputs, which are then encoded to803        # utf8 before passing them to os crypt() call.  this is done according to the "s" format804        # specifier for PyArg_ParseTuple (https://docs.python.org/3/c-api/arg.html).805        # There appears no way to get around that to pass raw bytes; so we just throw error here806        # to let user know they need to use another backend if they want raw bytes support.807        #808        # XXX: maybe just let safe_crypt() throw UnicodeDecodeError under passlib 2.0,809        #      and then catch it above? maybe have safe_crypt ALWAYS throw error810        #      instead of returning None? (would save re-detecting what went wrong)811        # XXX: isn't secret ALWAYS bytes at this point?812        #813        if PY3 and isinstance(secret, bytes):814            try:815                secret.decode("utf-8")816            except UnicodeDecodeError:817                raise error_from(uh.exc.PasswordValueError(818                    "python3 crypt.crypt() ony supports bytes passwords using UTF8; "819                    "passlib recommends running `pip install bcrypt` for general bcrypt support.",820                    ), None)821 822        #823        # else crypt() call failed for unknown reason.824        #825        # NOTE: getting here should be considered a bug in passlib --826        #       if os_crypt backend detection said there's support,827        #       and we've already checked all known reasons above;828        #       want them to file bug so we can figure out what happened.829        #       in the meantime, users can avoid this by installing bcrypt-cffi backend;830        #       which won't have this (or utf8) edgecases.831        #832        # XXX: throw something more specific, like an "InternalBackendError"?833        # NOTE: if do change this error, need to update test_81_crypt_fallback() expectations834        #       about what will be thrown; as well as safe_verify() above.835        #836        debug_only_repr = uh.exc.debug_only_repr837        raise uh.exc.InternalBackendError(838            "crypt.crypt() failed for unknown reason; "839            "passlib recommends running `pip install bcrypt` for general bcrypt support."840            # for debugging UTs --841            "(config=%s, secret=%s)" % (debug_only_repr(config), debug_only_repr(secret)),842            )843 844#-----------------------------------------------------------------------845# builtin backend846#-----------------------------------------------------------------------847class _BuiltinBackend(_BcryptCommon):848    """849    backend which uses passlib's pure-python implementation850    """851    @classmethod852    def _load_backend_mixin(mixin_cls, name, dryrun):853        from passlib.utils import as_bool854        if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")):855            log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT")856            return False857        global _builtin_bcrypt858        from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt859        return mixin_cls._finalize_backend_mixin(name, dryrun)860 861    def _calc_checksum(self, secret):862        secret, ident = self._prepare_digest_args(secret)863        chk = _builtin_bcrypt(secret, ident[1:-1],864                              self.salt.encode("ascii"), self.rounds)865        return chk.decode("ascii")866 867#=============================================================================868# handler869#=============================================================================870class bcrypt(_NoBackend, _BcryptCommon):871    """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`.872 873    It supports a fixed-length salt, and a variable number of rounds.874 875    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:876 877    :type salt: str878    :param salt:879        Optional salt string.880        If not specified, one will be autogenerated (this is recommended).881        If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.882 883    :type rounds: int884    :param rounds:885        Optional number of rounds to use.886        Defaults to 12, must be between 4 and 31, inclusive.887        This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`888        -- increasing the rounds by +1 will double the amount of time taken.889 890    :type ident: str891    :param ident:892        Specifies which version of the BCrypt algorithm will be used when creating a new hash.893        Typically this option is not needed, as the default (``"2b"``) is usually the correct choice.894        If specified, it must be one of the following:895 896        * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore.897        * ``"2a"`` - some implementations suffered from rare security flaws, replaced by 2b.898        * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation,899          identical to ``"2b"`` in all but name.900        * ``"2b"`` - latest revision of the official BCrypt algorithm, current default.901 902    :param bool truncate_error:903        By default, BCrypt will silently truncate passwords larger than 72 bytes.904        Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`905        to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.906 907        .. versionadded:: 1.7908 909    :type relaxed: bool910    :param relaxed:911        By default, providing an invalid value for one of the other912        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,913        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`914        will be issued instead. Correctable errors include ``rounds``915        that are too small or too large, and ``salt`` strings that are too long.916 917        .. versionadded:: 1.6918 919    .. versionchanged:: 1.6920        This class now supports ``"2y"`` hashes, and recognizes921        (but does not support) the broken ``"2x"`` hashes.922        (see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>`923        for details).924 925    .. versionchanged:: 1.6926        Added a pure-python backend.927 928    .. versionchanged:: 1.6.3929 930        Added support for ``"2b"`` variant.931 932    .. versionchanged:: 1.7933 934        Now defaults to ``"2b"`` variant.935    """936    #=============================================================================937    # backend938    #=============================================================================939 940    # NOTE: the brunt of the bcrypt class is implemented in _BcryptCommon.941    #       there are then subclass for each backend (e.g. _PyBcryptBackend),942    #       these are dynamically prepended to this class's bases943    #       in order to load the appropriate backend.944 945    #: list of potential backends946    backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin")947 948    #: flag that this class's bases should be modified by SubclassBackendMixin949    _backend_mixin_target = True950 951    #: map of backend -> mixin class, used by _get_backend_loader()952    _backend_mixin_map = {953        None: _NoBackend,954        "bcrypt": _BcryptBackend,955        "pybcrypt": _PyBcryptBackend,956        "bcryptor": _BcryptorBackend,957        "os_crypt": _OsCryptBackend,958        "builtin": _BuiltinBackend,959    }960 961    #=============================================================================962    # eoc963    #=============================================================================964 965#=============================================================================966# variants967#=============================================================================968_UDOLLAR = u("$")969 970# XXX: it might be better to have all the bcrypt variants share a common base class,971#      and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it.972class _wrapped_bcrypt(bcrypt):973    """974    abstracts out some bits bcrypt_sha256 & django_bcrypt_sha256 share.975    - bypass backend-loading wrappers for hash() etc976    - disable truncation support, sha256 wrappers don't need it.977    """978    setting_kwds = tuple(elem for elem in bcrypt.setting_kwds if elem not in ["truncate_error"])979    truncate_size = None980 981    # XXX: these will be needed if any bcrypt backends directly implement this...982    # @classmethod983    # def hash(cls, secret, **kwds):984    #     # bypass bcrypt backend overriding this method985    #     # XXX: would wrapping bcrypt make this easier than subclassing it?986    #     return super(_BcryptCommon, cls).hash(secret, **kwds)987    #988    # @classmethod989    # def verify(cls, secret, hash):990    #     # bypass bcrypt backend overriding this method991    #     return super(_BcryptCommon, cls).verify(secret, hash)992    #993    # @classmethod994    # def genhash(cls, secret, hash):995    #     # bypass bcrypt backend overriding this method996    #     return super(_BcryptCommon, cls).genhash(secret, hash)997 998    @classmethod999    def _check_truncate_policy(cls, secret):1000        # disable check performed by bcrypt(), since this doesn't truncate passwords.1001        pass1002 1003#=============================================================================1004# bcrypt sha256 wrapper1005#=============================================================================1006 1007class bcrypt_sha256(_wrapped_bcrypt):1008    """1009    This class implements a composition of BCrypt + HMAC_SHA256,1010    and follows the :ref:`password-hash-api`.1011 1012    It supports a fixed-length salt, and a variable number of rounds.1013 1014    The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept1015    all the same optional keywords as the base :class:`bcrypt` hash.1016 1017    .. versionadded:: 1.6.21018 1019    .. versionchanged:: 1.71020 1021        Now defaults to ``"2b"`` bcrypt variant; though supports older hashes1022        generated using the ``"2a"`` bcrypt variant.1023 1024    .. versionchanged:: 1.7.31025 1026        For increased security, updated to use HMAC-SHA256 instead of plain SHA256.1027        Now only supports the ``"2b"`` bcrypt variant.  Hash format updated to "v=2".1028    """1029    #===================================================================1030    # class attrs1031    #===================================================================1032 1033    #--------------------1034    # PasswordHash1035    #--------------------1036    name = "bcrypt_sha256"1037 1038    #--------------------1039    # GenericHandler1040    #--------------------1041    # this is locked at 2b for now (with 2a allowed only for legacy v1 format)1042    ident_values = (IDENT_2A, IDENT_2B)1043 1044    # clone bcrypt's ident aliases so they can be used here as well...1045    ident_aliases = (lambda ident_values: dict(item for item in bcrypt.ident_aliases.items()1046                                               if item[1] in ident_values))(ident_values)1047    default_ident = IDENT_2B1048 1049    #--------------------1050    # class specific1051    #--------------------1052 1053    _supported_versions = set([1, 2])1054 1055    #===================================================================1056    # instance attrs1057    #===================================================================1058 1059    #: wrapper version.1060    #: v1 -- used prior to passlib 1.7.3; performs ``bcrypt(sha256(secret), salt, cost)``1061    #: v2 -- new in passlib 1.7.3; performs `bcrypt(sha256_hmac(salt, secret), salt, cost)``1062    version = 21063 1064    #===================================================================1065    # configuration1066    #===================================================================1067 1068    @classmethod1069    def using(cls, version=None, **kwds):1070        subcls = super(bcrypt_sha256, cls).using(**kwds)1071        if version is not None:1072            subcls.version = subcls._norm_version(version)1073        ident = subcls.default_ident1074        if subcls.version > 1 and ident != IDENT_2B:1075            raise ValueError("bcrypt %r hashes not allowed for version %r" %1076                             (ident, subcls.version))1077        return subcls1078 1079    #===================================================================1080    # formatting1081    #===================================================================1082 1083    # sample hash:1084    # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu1085    # $bcrypt-sha256$           -- prefix/identifier1086    # 2a                        -- bcrypt variant1087    # ,                         -- field separator1088    # 6                         -- bcrypt work factor1089    # $                         -- section separator1090    # /3OeRpbOf8/l6nPPRdZPp.    -- salt1091    # $                         -- section separator1092    # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu  -- digest1093 1094    # XXX: we can't use .ident attr due to bcrypt code using it.1095    #      working around that via prefix.1096    prefix = u('$bcrypt-sha256$')1097 1098    #: current version 2 hash format1099    _v2_hash_re = re.compile(r"""(?x)1100        ^1101        [$]bcrypt-sha256[$]1102        v=(?P<version>\d+),1103        t=(?P<type>2b),1104        r=(?P<rounds>\d{1,2})1105        [$](?P<salt>[^$]{22})1106        (?:[$](?P<digest>[^$]{31}))?1107        $1108        """)1109 1110    #: old version 1 hash format1111    _v1_hash_re = re.compile(r"""(?x)1112        ^1113        [$]bcrypt-sha256[$]1114        (?P<type>2[ab]),1115        (?P<rounds>\d{1,2})1116        [$](?P<salt>[^$]{22})1117        (?:[$](?P<digest>[^$]{31}))?1118        $1119        """)1120 1121    @classmethod1122    def identify(cls, hash):1123        hash = uh.to_unicode_for_identify(hash)1124        if not hash:1125            return False1126        return hash.startswith(cls.prefix)1127 1128    @classmethod1129    def from_string(cls, hash):1130        hash = to_unicode(hash, "ascii", "hash")1131        if not hash.startswith(cls.prefix):1132            raise uh.exc.InvalidHashError(cls)1133        m = cls._v2_hash_re.match(hash)1134        if m:1135            version = int(m.group("version"))1136            if version < 2:1137                raise uh.exc.MalformedHashError(cls)1138        else:1139            m = cls._v1_hash_re.match(hash)1140            if m:1141                version = 11142            else:1143                raise uh.exc.MalformedHashError(cls)1144        rounds = m.group("rounds")1145        if rounds.startswith(uh._UZERO) and rounds != uh._UZERO:1146            raise uh.exc.ZeroPaddedRoundsError(cls)1147        return cls(1148            version=version,1149            ident=m.group("type"),1150            rounds=int(rounds),1151            salt=m.group("salt"),1152            checksum=m.group("digest"),1153        )1154 1155    _v2_template = u("$bcrypt-sha256$v=2,t=%s,r=%d$%s$%s")1156    _v1_template = u("$bcrypt-sha256$%s,%d$%s$%s")1157 1158    def to_string(self):1159        if self.version == 1:1160            template = self._v1_template1161        else:1162            template = self._v2_template1163        hash = template % (self.ident.strip(_UDOLLAR), self.rounds, self.salt, self.checksum)1164        return uascii_to_str(hash)1165 1166    #===================================================================1167    # init1168    #===================================================================1169 1170    def __init__(self, version=None, **kwds):1171        if version is not None:1172            self.version = self._norm_version(version)1173        super(bcrypt_sha256, self).__init__(**kwds)1174 1175    #===================================================================1176    # version1177    #===================================================================1178 1179    @classmethod1180    def _norm_version(cls, version):1181        if version not in cls._supported_versions:1182            raise ValueError("%s: unknown or unsupported version: %r" % (cls.name, version))1183        return version1184 1185    #===================================================================1186    # checksum1187    #===================================================================1188 1189    def _calc_checksum(self, secret):1190        # NOTE: can't use digest directly, since bcrypt stops at first NULL.1191        # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password1192        #       (XXX: citation needed), so we don't want key to be > 55 bytes.1193        #       thus, have to use base64 (44 bytes) rather than hex (64 bytes).1194        # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha2561195        #      may switch to hex encoding, since it's simpler to implement elsewhere.1196        if isinstance(secret, unicode):1197            secret = secret.encode("utf-8")1198 1199        if self.version == 1:1200            # version 1 -- old version just ran secret through sha256(),

Showing the first 1,200 of 1244 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai