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/9
7
8* deal with lack of PY3-compatibile c-ext implementation
9"""
10#=============================================================================
11# imports
12#=============================================================================
13from __future__ import with_statement, absolute_import
14# core
15from base64 import b64encode
16from hashlib import sha256
17import os
18import re
19import logging; log = logging.getLogger(__name__)
20from warnings import warn
21# site
22_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# pkg
26_builtin_bcrypt = None  # dynamically imported by _load_backend_builtin()
27from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError
28from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \
29                          rng, getrandstr, test_crypt, to_unicode
30from passlib.utils.binary import bcrypt64
31from passlib.utils.compat import get_unbound_method_function
32from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii
33import passlib.utils.handlers as uh
34
35# local
36__all__ = [
37    "bcrypt",
38]
39
40#=============================================================================
41# support funcs & constants
42#=============================================================================
43IDENT_2 = u("$2$")
44IDENT_2A = u("$2a$")
45IDENT_2X = u("$2x$")
46IDENT_2Y = u("$2y$")
47IDENT_2B = u("$2b$")
48_BNULL = b'\x00'
49
50# reference hash of "test", used in various self-checks
51TEST_HASH_2A = b"$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK"
52
53def _detect_pybcrypt():
54    """
55    internal helper which tries to distinguish pybcrypt vs bcrypt.
56
57    :returns:
58        True if cext-based py-bcrypt,
59        False if ffi-based bcrypt,
60        None if 'bcrypt' module not found.
61
62    .. versionchanged:: 1.6.3
63
64        Now assuming bcrypt installed, unless py-bcrypt explicitly detected.
65        Previous releases assumed py-bcrypt by default.
66
67        Making this change since py-bcrypt is (apparently) unmaintained and static,
68        whereas bcrypt is being actively maintained, and it's internal structure may shift.
69    """
70    # NOTE: this is also used by the unittests.
71
72    # check for module.
73    try:
74        import bcrypt
75    except ImportError:
76        # XXX: this is ignoring case where py-bcrypt's "bcrypt._bcrypt" C Ext fails to import;
77        #      would need to inspect actual ImportError message to catch that.
78        return None
79
80    # py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4),
81    # which bcrypt lacks (confirmed for v1.0 - 2.0)
82    # "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute.
83    try:
84        from bcrypt._bcrypt import __version__
85    except ImportError:
86        return False
87    return True
88
89#=============================================================================
90# backend mixins
91#=============================================================================
92class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents,
93                    uh.HasRounds, uh.HasSalt, uh.GenericHandler):
94    """
95    Base class which implements brunt of BCrypt code.
96    This is then subclassed by the various backends,
97    to override w/ backend-specific methods.
98
99    When a backend is loaded, the bases of the 'bcrypt' class proper
100    are modified to prepend the correct backend-specific subclass.
101    """
102    #===================================================================
103    # class attrs
104    #===================================================================
105
106    #--------------------
107    # PasswordHash
108    #--------------------
109    name = "bcrypt"
110    setting_kwds = ("salt", "rounds", "ident", "truncate_error")
111
112    #--------------------
113    # GenericHandler
114    #--------------------
115    checksum_size = 31
116    checksum_chars = bcrypt64.charmap
117
118    #--------------------
119    # HasManyIdents
120    #--------------------
121    default_ident = IDENT_2B
122    ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B)
123    ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A,  u("2y"): IDENT_2Y,
124                     u("2b"): IDENT_2B}
125
126    #--------------------
127    # HasSalt
128    #--------------------
129    min_salt_size = max_salt_size = 22
130    salt_chars = bcrypt64.charmap
131        # NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap
132
133    #--------------------
134    # HasRounds
135    #--------------------
136    default_rounds = 12 # current passlib default
137    min_rounds = 4 # minimum from bcrypt specification
138    max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)
139    rounds_cost = "log2"
140
141    #--------------------
142    # TruncateMixin
143    #--------------------
144    truncate_size = 72
145
146    #--------------------
147    # custom
148    #--------------------
149
150    # backend workaround detection flags
151    # NOTE: these are only set on the backend mixin classes
152    _workrounds_initialized = False
153    _has_2a_wraparound_bug = False
154    _lacks_20_support = False
155    _lacks_2y_support = False
156    _lacks_2b_support = False
157    _fallback_ident = IDENT_2A
158
159    #===================================================================
160    # formatting
161    #===================================================================
162
163    @classmethod
164    def from_string(cls, hash):
165        ident, tail = cls._parse_ident(hash)
166        if ident == IDENT_2X:
167            raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
168                             "currently supported")
169        rounds_str, data = tail.split(u("$"))
170        rounds = int(rounds_str)
171        if rounds_str != u('%02d') % (rounds,):
172            raise uh.exc.MalformedHashError(cls, "malformed cost field")
173        salt, chk = data[:22], data[22:]
174        return cls(
175            rounds=rounds,
176            salt=salt,
177            checksum=chk or None,
178            ident=ident,
179        )
180
181    def to_string(self):
182        hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum)
183        return uascii_to_str(hash)
184
185    # NOTE: this should be kept separate from to_string()
186    #       so that bcrypt_sha256() can still use it, while overriding to_string()
187    def _get_config(self, ident):
188        """internal helper to prepare config string for backends"""
189        config = u("%s%02d$%s") % (ident, self.rounds, self.salt)
190        return uascii_to_str(config)
191
192    #===================================================================
193    # migration
194    #===================================================================
195
196    @classmethod
197    def needs_update(cls, hash, **kwds):
198        # check for incorrect padding bits (passlib issue 25)
199        if isinstance(hash, bytes):
200            hash = hash.decode("ascii")
201        if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]:
202            return True
203
204        # TODO: try to detect incorrect 8bit/wraparound hashes using kwds.get("secret")
205
206        # hand off to base implementation, so HasRounds can check rounds value.
207        return super(_BcryptCommon, cls).needs_update(hash, **kwds)
208
209    #===================================================================
210    # specialized salt generation - fixes passlib issue 25
211    #===================================================================
212
213    @classmethod
214    def normhash(cls, hash):
215        """helper to normalize hash, correcting any bcrypt padding bits"""
216        if cls.identify(hash):
217            return cls.from_string(hash).to_string()
218        else:
219            return hash
220
221    @classmethod
222    def _generate_salt(cls):
223        # generate random salt as normal,
224        # but repair last char so the padding bits always decode to zero.
225        salt = super(_BcryptCommon, cls)._generate_salt()
226        return bcrypt64.repair_unused(salt)
227
228    @classmethod
229    def _norm_salt(cls, salt, **kwds):
230        salt = super(_BcryptCommon, cls)._norm_salt(salt, **kwds)
231        assert salt is not None, "HasSalt didn't generate new salt!"
232        changed, salt = bcrypt64.check_repair_unused(salt)
233        if changed:
234            # FIXME: if salt was provided by user, this message won't be
235            # correct. not sure if we want to throw error, or use different warning.
236            warn(
237                "encountered a bcrypt salt with incorrectly set padding bits; "
238                "you may want to use bcrypt.normhash() "
239                "to fix this; this will be an error under Passlib 2.0",
240                PasslibHashWarning)
241        return salt
242
243    def _norm_checksum(self, checksum, relaxed=False):
244        checksum = super(_BcryptCommon, self)._norm_checksum(checksum, relaxed=relaxed)
245        changed, checksum = bcrypt64.check_repair_unused(checksum)
246        if changed:
247            warn(
248                "encountered a bcrypt hash with incorrectly set padding bits; "
249                "you may want to use bcrypt.normhash() "
250                "to fix this; this will be an error under Passlib 2.0",
251                PasslibHashWarning)
252        return checksum
253
254    #===================================================================
255    # backend configuration
256    # NOTE: backends are defined in terms of mixin classes,
257    #       which are dynamically inserted into the bases of the 'bcrypt' class
258    #       via the machinery in 'SubclassBackendMixin'.
259    #       this lets us load in a backend-specific implementation
260    #       of _calc_checksum() and similar methods.
261    #===================================================================
262
263    # NOTE: backend config is located down in <bcrypt> class
264
265    # NOTE: set_backend() will execute the ._load_backend_mixin()
266    #       of the matching mixin class, which will handle backend detection
267
268    # appended to HasManyBackends' "no backends available" error message
269    _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')"
270
271    @classmethod
272    def _finalize_backend_mixin(mixin_cls, backend, dryrun):
273        """
274        helper called by from backend mixin classes' _load_backend_mixin() --
275        invoked after backend imports have been loaded, and performs
276        feature detection & testing common to all backends.
277        """
278        #----------------------------------------------------------------
279        # setup helpers
280        #----------------------------------------------------------------
281        assert mixin_cls is bcrypt._backend_mixin_map[backend], \
282            "_configure_workarounds() invoked from wrong class"
283
284        if mixin_cls._workrounds_initialized:
285            return True
286
287        verify = mixin_cls.verify
288
289        err_types = (ValueError,)
290        if _bcryptor:
291            err_types += (_bcryptor.engine.SaltError,)
292
293        def safe_verify(secret, hash):
294            """verify() wrapper which traps 'unknown identifier' errors"""
295            try:
296                return verify(secret, hash)
297            except err_types:
298                # backends without support for given ident will throw various
299                # errors about unrecognized version:
300                #   pybcrypt, bcrypt -- raises ValueError
301                #   bcryptor -- raises bcryptor.engine.SaltError
302                return NotImplemented
303            except AssertionError as err:
304                # _calc_checksum() code may also throw AssertionError
305                # if correct hash isn't returned (e.g. 2y hash converted to 2b,
306                # such as happens with bcrypt 3.0.0)
307                log.debug("trapped unexpected response from %r backend: verify(%r, %r):",
308                          backend, secret, hash, exc_info=True)
309                return NotImplemented
310
311        def assert_lacks_8bit_bug(ident):
312            """
313            helper to check for cryptblowfish 8bit bug (fixed in 2y/2b);
314            even though it's not known to be present in any of passlib's backends.
315            this is treated as FATAL, because it can easily result in seriously malformed hashes,
316            and we can't correct for it ourselves.
317
318            test cases from <http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/wrapper.c.diff?r1=1.9;r2=1.10>
319            reference hash is the incorrectly generated $2x$ hash taken from above url
320            """
321            secret = b"\xA3"
322            bug_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"
323            if verify(secret, bug_hash):
324                # NOTE: this only EVER be observed in 2a hashes,
325                #       2y/2b hashes should have fixed the bug.
326                #       (but we check w/ them anyways).
327                raise PasslibSecurityError(
328                    "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
329                    "the crypt_blowfish 8-bit bug (CVE-2011-2483), "
330                    "and should be upgraded or replaced with another backend." % backend)
331
332            # if it doesn't have wraparound bug, make sure it *does* handle things
333            # correctly -- or we're in some weird third case.
334            correct_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq"
335            if not verify(secret, correct_hash):
336                raise RuntimeError("%s backend failed to verify %s 8bit hash" % (backend, ident))
337
338        def detect_wrap_bug(ident):
339            """
340            check for bsd wraparound bug (fixed in 2b)
341            this is treated as a warning, because it's rare in the field,
342            and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it.
343
344            test cases from <http://www.openwall.com/lists/oss-security/2012/01/02/4>
345
346            NOTE: reference hash is of password "0"*72
347
348            NOTE: if in future we need to deliberately create hashes which have this bug,
349                  can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)'
350            """
351            # check if it exhibits wraparound bug
352            secret = (b"0123456789"*26)[:255]
353            bug_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"
354            if verify(secret, bug_hash):
355                return True
356
357            # if it doesn't have wraparound bug, make sure it *does* handle things
358            # correctly -- or we're in some weird third case.
359            correct_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi"
360            if not verify(secret, correct_hash):
361                raise RuntimeError("%s backend failed to verify %s wraparound hash" % (backend, ident))
362
363            return False
364
365        def assert_lacks_wrap_bug(ident):
366            if not detect_wrap_bug(ident):
367                return
368            # should only see in 2a, later idents should NEVER exhibit this bug:
369            # * 2y implementations should have been free of it
370            # * 2b was what (supposedly) fixed it
371            raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident))
372
373        #----------------------------------------------------------------
374        # check for old 20 support
375        #----------------------------------------------------------------
376        test_hash_20 = b"$2$04$5BJqKfqMQvV7nS.yUguNcuRfMMOXK0xPWavM7pOzjEi5ze5T1k8/S"
377        result = safe_verify("test", test_hash_20)
378        if not result:
379            raise RuntimeError("%s incorrectly rejected $2$ hash" % backend)
380        elif result is NotImplemented:
381            mixin_cls._lacks_20_support = True
382            log.debug("%r backend lacks $2$ support, enabling workaround", backend)
383
384        #----------------------------------------------------------------
385        # check for 2a support
386        #----------------------------------------------------------------
387        result = safe_verify("test", TEST_HASH_2A)
388        if not result:
389            raise RuntimeError("%s incorrectly rejected $2a$ hash" % backend)
390        elif result is NotImplemented:
391            # 2a support is required, and should always be present
392            raise RuntimeError("%s lacks support for $2a$ hashes" % backend)
393        else:
394            assert_lacks_8bit_bug(IDENT_2A)
395            if detect_wrap_bug(IDENT_2A):
396                warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
397                     "the bsd wraparound bug, "
398                     "and should be upgraded or replaced with another backend "
399                     "(enabling workaround for now)." % backend,
400                     uh.exc.PasslibSecurityWarning)
401                mixin_cls._has_2a_wraparound_bug = True
402
403        #----------------------------------------------------------------
404        # check for 2y support
405        #----------------------------------------------------------------
406        test_hash_2y = TEST_HASH_2A.replace(b"2a", b"2y")
407        result = safe_verify("test", test_hash_2y)
408        if not result:
409            raise RuntimeError("%s incorrectly rejected $2y$ hash" % backend)
410        elif result is NotImplemented:
411            mixin_cls._lacks_2y_support = True
412            log.debug("%r backend lacks $2y$ support, enabling workaround", backend)
413        else:
414            # NOTE: Not using this as fallback candidate,
415            #       lacks wide enough support across implementations.
416            assert_lacks_8bit_bug(IDENT_2Y)
417            assert_lacks_wrap_bug(IDENT_2Y)
418
419        #----------------------------------------------------------------
420        # TODO: check for 2x support
421        #----------------------------------------------------------------
422
423        #----------------------------------------------------------------
424        # check for 2b support
425        #----------------------------------------------------------------
426        test_hash_2b = TEST_HASH_2A.replace(b"2a", b"2b")
427        result = safe_verify("test", test_hash_2b)
428        if not result:
429            raise RuntimeError("%s incorrectly rejected $2b$ hash" % backend)
430        elif result is NotImplemented:
431            mixin_cls._lacks_2b_support = True
432            log.debug("%r backend lacks $2b$ support, enabling workaround", backend)
433        else:
434            mixin_cls._fallback_ident = IDENT_2B
435            assert_lacks_8bit_bug(IDENT_2B)
436            assert_lacks_wrap_bug(IDENT_2B)
437
438        # set flag so we don't have to run this again
439        mixin_cls._workrounds_initialized = True
440        return True
441
442    #===================================================================
443    # digest calculation
444    #===================================================================
445
446    # _calc_checksum() defined by backends
447
448    def _prepare_digest_args(self, secret):
449        """
450        common helper for backends to implement _calc_checksum().
451        takes in secret, returns (secret, ident) pair,
452        """
453        return self._norm_digest_args(secret, self.ident, new=self.use_defaults)
454
455    @classmethod
456    def _norm_digest_args(cls, secret, ident, new=False):
457        # make sure secret is unicode
458        if isinstance(secret, unicode):
459            secret = secret.encode("utf-8")
460
461        # check max secret size
462        uh.validate_secret(secret)
463
464        # check for truncation (during .hash() calls only)
465        if new:
466            cls._check_truncate_policy(secret)
467
468        # NOTE: especially important to forbid NULLs for bcrypt, since many
469        # backends (bcryptor, bcrypt) happily accept them, and then
470        # silently truncate the password at first NULL they encounter!
471        if _BNULL in secret:
472            raise uh.exc.NullPasswordError(cls)
473
474        # TODO: figure out way to skip these tests when not needed...
475
476        # protect from wraparound bug by truncating secret before handing it to the backend.
477        # bcrypt only uses first 72 bytes anyways.
478        # NOTE: not needed for 2y/2b, but might use 2a as fallback for them.
479        if cls._has_2a_wraparound_bug and len(secret) >= 255:
480            secret = secret[:72]
481
482        # special case handling for variants (ordered most common first)
483        if ident == IDENT_2A:
484            # nothing needs to be done.
485            pass
486
487        elif ident == IDENT_2B:
488            if cls._lacks_2b_support:
489                # handle $2b$ hash format even if backend is too old.
490                # have it generate a 2A/2Y digest, then return it as a 2B hash.
491                # 2a-only backend could potentially exhibit wraparound bug --
492                # but we work around that issue above.
493                ident = cls._fallback_ident
494
495        elif ident == IDENT_2Y:
496            if cls._lacks_2y_support:
497                # handle $2y$ hash format (not supported by BSDs, being phased out on others)
498                # have it generate a 2A/2B digest, then return it as a 2Y hash.
499                ident = cls._fallback_ident
500
501        elif ident == IDENT_2:
502            if cls._lacks_20_support:
503                # handle legacy $2$ format (not supported by most backends except BSD os_crypt)
504                # we can fake $2$ behavior using the 2A/2Y/2B algorithm
505                # by repeating the password until it's at least 72 chars in length.
506                if secret:
507                    secret = repeat_string(secret, 72)
508                ident = cls._fallback_ident
509
510        elif ident == IDENT_2X:
511
512            # NOTE: shouldn't get here.
513            # XXX: could check if backend does actually offer 'support'
514            raise RuntimeError("$2x$ hashes not currently supported by passlib")
515
516        else:
517            raise AssertionError("unexpected ident value: %r" % ident)
518
519        return secret, ident
520
521#-----------------------------------------------------------------------
522# stub backend
523#-----------------------------------------------------------------------
524class _NoBackend(_BcryptCommon):
525    """
526    mixin used before any backend has been loaded.
527    contains stubs that force loading of one of the available backends.
528    """
529    #===================================================================
530    # digest calculation
531    #===================================================================
532    def _calc_checksum(self, secret):
533        self._stub_requires_backend()
534        # NOTE: have to use super() here so that we don't recursively
535        #       call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum
536        return super(bcrypt, self)._calc_checksum(secret)
537
538    #===================================================================
539    # eoc
540    #===================================================================
541
542#-----------------------------------------------------------------------
543# bcrypt backend
544#-----------------------------------------------------------------------
545class _BcryptBackend(_BcryptCommon):
546    """
547    backend which uses 'bcrypt' package
548    """
549
550    @classmethod
551    def _load_backend_mixin(mixin_cls, name, dryrun):
552        # try to import bcrypt
553        global _bcrypt
554        if _detect_pybcrypt():
555            # pybcrypt was installed instead
556            return False
557        try:
558            import bcrypt as _bcrypt
559        except ImportError: # pragma: no cover
560            return False
561        try:
562            version = _bcrypt.__about__.__version__
563        except:
564            log.warning("(trapped) error reading bcrypt version", exc_info=True)
565            version = '<unknown>'
566
567        log.debug("detected 'bcrypt' backend, version %r", version)
568        return mixin_cls._finalize_backend_mixin(name, dryrun)
569
570    # # TODO: would like to implementing verify() directly,
571    # #       to skip need for parsing hash strings.
572    # #       below method has a few edge cases where it chokes though.
573    # @classmethod
574    # def verify(cls, secret, hash):
575    #     if isinstance(hash, unicode):
576    #         hash = hash.encode("ascii")
577    #     ident = hash[:hash.index(b"$", 1)+1].decode("ascii")
578    #     if ident not in cls.ident_values:
579    #         raise uh.exc.InvalidHashError(cls)
580    #     secret, eff_ident = cls._norm_digest_args(secret, ident)
581    #     if eff_ident != ident:
582    #         # lacks support for original ident, replace w/ new one.
583    #         hash = eff_ident.encode("ascii") + hash[len(ident):]
584    #     result = _bcrypt.hashpw(secret, hash)
585    #     assert result.startswith(eff_ident)
586    #     return consteq(result, hash)
587
588    def _calc_checksum(self, secret):
589        # bcrypt behavior:
590        #   secret must be bytes
591        #   config must be ascii bytes
592        #   returns ascii bytes
593        secret, ident = self._prepare_digest_args(secret)
594        config = self._get_config(ident)
595        if isinstance(config, unicode):
596            config = config.encode("ascii")
597        hash = _bcrypt.hashpw(secret, config)
598        assert hash.startswith(config) and len(hash) == len(config)+31, \
599            "config mismatch: %r => %r" % (config, hash)
600        assert isinstance(hash, bytes)
601        return hash[-31:].decode("ascii")
602
603#-----------------------------------------------------------------------
604# bcryptor backend
605#-----------------------------------------------------------------------
606class _BcryptorBackend(_BcryptCommon):
607    """
608    backend which uses 'bcryptor' package
609    """
610
611    @classmethod
612    def _load_backend_mixin(mixin_cls, name, dryrun):
613        # try to import bcryptor
614        global _bcryptor
615        try:
616            import bcryptor as _bcryptor
617        except ImportError: # pragma: no cover
618            return False
619
620        # deprecated as of 1.7.2
621        if not dryrun:
622            warn("Support for `bcryptor` is deprecated, and will be removed in Passlib 1.8; "
623                 "Please use `pip install bcrypt` instead", DeprecationWarning)
624
625        return mixin_cls._finalize_backend_mixin(name, dryrun)
626
627    def _calc_checksum(self, secret):
628        # bcryptor behavior:
629        #   py2: unicode secret/hash encoded as ascii bytes before use,
630        #        bytes taken as-is; returns ascii bytes.
631        #   py3: not supported
632        secret, ident = self._prepare_digest_args(secret)
633        config = self._get_config(ident)
634        hash = _bcryptor.engine.Engine(False).hash_key(secret, config)
635        assert hash.startswith(config) and len(hash) == len(config)+31
636        return str_to_uascii(hash[-31:])
637
638#-----------------------------------------------------------------------
639# pybcrypt backend
640#-----------------------------------------------------------------------
641class _PyBcryptBackend(_BcryptCommon):
642    """
643    backend which uses 'pybcrypt' package
644    """
645
646    #: classwide thread lock used for pybcrypt < 0.3
647    _calc_lock = None
648
649    @classmethod
650    def _load_backend_mixin(mixin_cls, name, dryrun):
651        # try to import pybcrypt
652        global _pybcrypt
653        if not _detect_pybcrypt():
654            # not installed, or bcrypt installed instead
655            return False
656        try:
657            import bcrypt as _pybcrypt
658        except ImportError: # pragma: no cover
659            # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken)
660            return False
661
662        # deprecated as of 1.7.2
663        if not dryrun:
664            warn("Support for `py-bcrypt` is deprecated, and will be removed in Passlib 1.8; "
665                 "Please use `pip install bcrypt` instead", DeprecationWarning)
666
667        # determine pybcrypt version
668        try:
669            version = _pybcrypt._bcrypt.__version__
670        except:
671            log.warning("(trapped) error reading pybcrypt version", exc_info=True)
672            version = "<unknown>"
673        log.debug("detected 'pybcrypt' backend, version %r", version)
674
675        # return calc function based on version
676        vinfo = parse_version(version) or (0, 0)
677        if vinfo < (0, 3):
678            warn("py-bcrypt %s has a major security vulnerability, "
679                 "you should upgrade to py-bcrypt 0.3 immediately."
680                 % version, uh.exc.PasslibSecurityWarning)
681            if mixin_cls._calc_lock is None:
682                import threading
683                mixin_cls._calc_lock = threading.Lock()
684            mixin_cls._calc_checksum = get_unbound_method_function(mixin_cls._calc_checksum_threadsafe)
685
686        return mixin_cls._finalize_backend_mixin(name, dryrun)
687
688    def _calc_checksum_threadsafe(self, secret):
689        # as workaround for pybcrypt < 0.3's concurrency issue,
690        # we wrap everything in a thread lock. as long as bcrypt is only
691        # used through passlib, this should be safe.
692        with self._calc_lock:
693            return self._calc_checksum_raw(secret)
694
695    def _calc_checksum_raw(self, secret):
696        # py-bcrypt behavior:
697        #   py2: unicode secret/hash encoded as ascii bytes before use,
698        #        bytes taken as-is; returns ascii bytes.
699        #   py3: unicode secret encoded as utf-8 bytes,
700        #        hash encoded as ascii bytes, returns ascii unicode.
701        secret, ident = self._prepare_digest_args(secret)
702        config = self._get_config(ident)
703        hash = _pybcrypt.hashpw(secret, config)
704        assert hash.startswith(config) and len(hash) == len(config)+31
705        return str_to_uascii(hash[-31:])
706
707    _calc_checksum = _calc_checksum_raw
708
709#-----------------------------------------------------------------------
710# os crypt backend
711#-----------------------------------------------------------------------
712class _OsCryptBackend(_BcryptCommon):
713    """
714    backend which uses :func:`crypt.crypt`
715    """
716
717    @classmethod
718    def _load_backend_mixin(mixin_cls, name, dryrun):
719        if not test_crypt("test", TEST_HASH_2A):
720            return False
721        return mixin_cls._finalize_backend_mixin(name, dryrun)
722
723    def _calc_checksum(self, secret):
724        secret, ident = self._prepare_digest_args(secret)
725        config = self._get_config(ident)
726        hash = safe_crypt(secret, config)
727        if hash:
728            assert hash.startswith(config) and len(hash) == len(config)+31
729            return hash[-31:]
730        else:
731            # NOTE: Have to raise this error because python3's crypt.crypt() only accepts unicode.
732            #       This means it can't handle any passwords that aren't either unicode
733            #       or utf-8 encoded bytes.  However, hashing a password with an alternate
734            #       encoding should be a pretty rare edge case; if user needs it, they can just
735            #       install bcrypt backend.
736            # XXX: is this the right error type to raise?
737            #      maybe have safe_crypt() not swallow UnicodeDecodeError, and have handlers
738            #      like sha256_crypt trap it if they have alternate method of handling them?
739            raise uh.exc.MissingBackendError(
740                "non-utf8 encoded passwords can't be handled by crypt.crypt() under python3, "
741                "recommend running `pip install bcrypt`.",
742                )
743
744#-----------------------------------------------------------------------
745# builtin backend
746#-----------------------------------------------------------------------
747class _BuiltinBackend(_BcryptCommon):
748    """
749    backend which uses passlib's pure-python implementation
750    """
751    @classmethod
752    def _load_backend_mixin(mixin_cls, name, dryrun):
753        from passlib.utils import as_bool
754        if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")):
755            log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT")
756            return False
757        global _builtin_bcrypt
758        from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt
759        return mixin_cls._finalize_backend_mixin(name, dryrun)
760
761    def _calc_checksum(self, secret):
762        secret, ident = self._prepare_digest_args(secret)
763        chk = _builtin_bcrypt(secret, ident[1:-1],
764                              self.salt.encode("ascii"), self.rounds)
765        return chk.decode("ascii")
766
767#=============================================================================
768# handler
769#=============================================================================
770class bcrypt(_NoBackend, _BcryptCommon):
771    """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`.
772
773    It supports a fixed-length salt, and a variable number of rounds.
774
775    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
776
777    :type salt: str
778    :param salt:
779        Optional salt string.
780        If not specified, one will be autogenerated (this is recommended).
781        If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
782
783    :type rounds: int
784    :param rounds:
785        Optional number of rounds to use.
786        Defaults to 12, must be between 4 and 31, inclusive.
787        This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`
788        -- increasing the rounds by +1 will double the amount of time taken.
789
790    :type ident: str
791    :param ident:
792        Specifies which version of the BCrypt algorithm will be used when creating a new hash.
793        Typically this option is not needed, as the default (``"2b"``) is usually the correct choice.
794        If specified, it must be one of the following:
795
796        * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore.
797        * ``"2a"`` - some implementations suffered from rare security flaws, replaced by 2b.
798        * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation,
799          identical to ``"2b"`` in all but name.
800        * ``"2b"`` - latest revision of the official BCrypt algorithm, current default.
801
802    :param bool truncate_error:
803        By default, BCrypt will silently truncate passwords larger than 72 bytes.
804        Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
805        to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
806
807        .. versionadded:: 1.7
808
809    :type relaxed: bool
810    :param relaxed:
811        By default, providing an invalid value for one of the other
812        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
813        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
814        will be issued instead. Correctable errors include ``rounds``
815        that are too small or too large, and ``salt`` strings that are too long.
816
817        .. versionadded:: 1.6
818
819    .. versionchanged:: 1.6
820        This class now supports ``"2y"`` hashes, and recognizes
821        (but does not support) the broken ``"2x"`` hashes.
822        (see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>`
823        for details).
824
825    .. versionchanged:: 1.6
826        Added a pure-python backend.
827
828    .. versionchanged:: 1.6.3
829
830        Added support for ``"2b"`` variant.
831
832    .. versionchanged:: 1.7
833
834        Now defaults to ``"2b"`` variant.
835    """
836    #=============================================================================
837    # backend
838    #=============================================================================
839
840    # NOTE: the brunt of the bcrypt class is implemented in _BcryptCommon.
841    #       there are then subclass for each backend (e.g. _PyBcryptBackend),
842    #       these are dynamically prepended to this class's bases
843    #       in order to load the appropriate backend.
844
845    #: list of potential backends
846    backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin")
847
848    #: flag that this class's bases should be modified by SubclassBackendMixin
849    _backend_mixin_target = True
850
851    #: map of backend -> mixin class, used by _get_backend_loader()
852    _backend_mixin_map = {
853        None: _NoBackend,
854        "bcrypt": _BcryptBackend,
855        "pybcrypt": _PyBcryptBackend,
856        "bcryptor": _BcryptorBackend,
857        "os_crypt": _OsCryptBackend,
858        "builtin": _BuiltinBackend,
859    }
860
861    #=============================================================================
862    # eoc
863    #=============================================================================
864
865#=============================================================================
866# variants
867#=============================================================================
868_UDOLLAR = u("$")
869
870# XXX: it might be better to have all the bcrypt variants share a common base class,
871#      and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it.
872class _wrapped_bcrypt(bcrypt):
873    """
874    abstracts out some bits bcrypt_sha256 & django_bcrypt_sha256 share.
875    - bypass backend-loading wrappers for hash() etc
876    - disable truncation support, sha256 wrappers don't need it.
877    """
878    setting_kwds = tuple(elem for elem in bcrypt.setting_kwds if elem not in ["truncate_error"])
879    truncate_size = None
880
881    # XXX: these will be needed if any bcrypt backends directly implement this...
882    # @classmethod
883    # def hash(cls, secret, **kwds):
884    #     # bypass bcrypt backend overriding this method
885    #     # XXX: would wrapping bcrypt make this easier than subclassing it?
886    #     return super(_BcryptCommon, cls).hash(secret, **kwds)
887    #
888    # @classmethod
889    # def verify(cls, secret, hash):
890    #     # bypass bcrypt backend overriding this method
891    #     return super(_BcryptCommon, cls).verify(secret, hash)
892    #
893    # @classmethod
894    # def genhash(cls, secret, hash):
895    #     # bypass bcrypt backend overriding this method
896    #     return super(_BcryptCommon, cls).genhash(secret, hash)
897
898    @classmethod
899    def _check_truncate_policy(cls, secret):
900        # disable check performed by bcrypt(), since this doesn't truncate passwords.
901        pass
902
903#=============================================================================
904# bcrypt sha256 wrapper
905#=============================================================================
906
907class bcrypt_sha256(_wrapped_bcrypt):
908    """This class implements a composition of BCrypt+SHA256, and follows the :ref:`password-hash-api`.
909
910    It supports a fixed-length salt, and a variable number of rounds.
911
912    The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept
913    all the same optional keywords as the base :class:`bcrypt` hash.
914
915    .. versionadded:: 1.6.2
916
917    .. versionchanged:: 1.7
918
919        Now defaults to ``"2b"`` variant.
920    """
921    #===================================================================
922    # class attrs
923    #===================================================================
924
925    #--------------------
926    # PasswordHash
927    #--------------------
928    name = "bcrypt_sha256"
929
930    #--------------------
931    # GenericHandler
932    #--------------------
933    # this is locked at 2a/2b for now.
934    ident_values = (IDENT_2A, IDENT_2B)
935
936    # clone bcrypt's ident aliases so they can be used here as well...
937    ident_aliases = (lambda ident_values: dict(item for item in bcrypt.ident_aliases.items()
938                                               if item[1] in ident_values))(ident_values)
939    default_ident = IDENT_2B
940
941    #===================================================================
942    # formatting
943    #===================================================================
944
945    # sample hash:
946    # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
947    # $bcrypt-sha256$           -- prefix/identifier
948    # 2a                        -- bcrypt variant
949    # ,                         -- field separator
950    # 6                         -- bcrypt work factor
951    # $                         -- section separator
952    # /3OeRpbOf8/l6nPPRdZPp.    -- salt
953    # $                         -- section separator
954    # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu  -- digest
955
956    # XXX: we can't use .ident attr due to bcrypt code using it.
957    #      working around that via prefix.
958    prefix = u('$bcrypt-sha256$')
959
960    _hash_re = re.compile(r"""
961        ^
962        [$]bcrypt-sha256
963        [$](?P<variant>2[ab])
964        ,(?P<rounds>\d{1,2})
965        [$](?P<salt>[^$]{22})
966        (?:[$](?P<digest>.{31}))?
967        $
968        """, re.X)
969
970    @classmethod
971    def identify(cls, hash):
972        hash = uh.to_unicode_for_identify(hash)
973        if not hash:
974            return False
975        return hash.startswith(cls.prefix)
976
977    @classmethod
978    def from_string(cls, hash):
979        hash = to_unicode(hash, "ascii", "hash")
980        if not hash.startswith(cls.prefix):
981            raise uh.exc.InvalidHashError(cls)
982        m = cls._hash_re.match(hash)
983        if not m:
984            raise uh.exc.MalformedHashError(cls)
985        rounds = m.group("rounds")
986        if rounds.startswith(uh._UZERO) and rounds != uh._UZERO:
987            raise uh.exc.ZeroPaddedRoundsError(cls)
988        return cls(ident=m.group("variant"),
989                   rounds=int(rounds),
990                   salt=m.group("salt"),
991                   checksum=m.group("digest"),
992                   )
993
994    _template = u("$bcrypt-sha256$%s,%d$%s$%s")
995
996    def to_string(self):
997        hash = self._template % (self.ident.strip(_UDOLLAR),
998                                 self.rounds, self.salt, self.checksum)
999        return uascii_to_str(hash)
1000
1001    #===================================================================
1002    # checksum
1003    #===================================================================
1004    def _calc_checksum(self, secret):
1005        # NOTE: can't use digest directly, since bcrypt stops at first NULL.
1006        # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password
1007        #       (XXX: citation needed), so we don't want key to be > 55 bytes.
1008        #       thus, have to use base64 (44 bytes) rather than hex (64 bytes).
1009        # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha256
1010        #      may switch to hex encoding, since it's simpler to implement elsewhere.
1011        if isinstance(secret, unicode):
1012            secret = secret.encode("utf-8")
1013
1014        # NOTE: output of b64encode() uses "+/" altchars, "=" padding chars,
1015        #       and no leading/trailing whitespace.
1016        key = b64encode(sha256(secret).digest())
1017
1018        # hand result off to normal bcrypt algorithm
1019        return super(bcrypt_sha256, self)._calc_checksum(key)
1020
1021    #===================================================================
1022    # other
1023    #===================================================================
1024
1025    # XXX: have _needs_update() mark the $2a$ ones for upgrading?
1026    #      maybe do that after we switch to hex encoding?
1027
1028    #===================================================================
1029    # eoc
1030    #===================================================================
1031
1032#=============================================================================
1033# eof
1034#=============================================================================
1035