1"""passlib.tests.test_handlers - tests for passlib hash algorithms"""
2#=============================================================================
3# imports
4#=============================================================================
5from __future__ import with_statement
6# core
7import logging; log = logging.getLogger(__name__)
8import os
9import warnings
10# site
11# pkg
12from passlib import hash
13from passlib.handlers.bcrypt import IDENT_2, IDENT_2X
14from passlib.utils import repeat_string, to_bytes, is_safe_crypt_input
15from passlib.utils.compat import irange, PY3
16from passlib.tests.utils import HandlerCase, TEST_MODE
17from passlib.tests.test_handlers import UPASS_TABLE
18# module
19
20#=============================================================================
21# bcrypt
22#=============================================================================
23class _bcrypt_test(HandlerCase):
24    """base for BCrypt test cases"""
25    handler = hash.bcrypt
26    reduce_default_rounds = True
27    fuzz_salts_need_bcrypt_repair = True
28
29    known_correct_hashes = [
30        #
31        # from JTR 1.7.9
32        #
33        ('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'),
34        ('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'),
35        ('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'),
36        ('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'),
37        ('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'),
38
39        #
40        # test vectors from http://www.openwall.com/crypt v1.2
41        # note that this omits any hashes that depend on crypt_blowfish's
42        # various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password,
43        # and any 2x hashes); and only contain hashes which are correct
44        # under both crypt_blowfish 1.2 AND OpenBSD.
45        #
46        ('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'),
47        ('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'),
48        ('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'),
49        ('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'),
50        ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
51         '0123456789chars after 72 are ignored',
52                '$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'),
53        (b'\xa3',
54                '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
55        (b'\xff\xa3345',
56            '$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'),
57        (b'\xa3ab',
58                '$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'),
59        (b'\xaa'*72 + b'chars after 72 are ignored as usual',
60                '$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'),
61        (b'\xaa\x55'*36,
62                '$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'),
63        (b'\x55\xaa\xff'*24,
64                '$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'),
65
66        # keeping one of their 2y tests, because we are supporting that.
67        (b'\xa3',
68                '$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
69
70        #
71        # 8bit bug (fixed in 2y/2b)
72        #
73
74        # NOTE: see assert_lacks_8bit_bug() for origins of this test vector.
75        (b"\xd1\x91", "$2y$05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu"),
76
77        #
78        # bsd wraparound bug (fixed in 2b)
79        #
80
81        # NOTE: if backend is vulnerable, password will hash the same as '0'*72
82        #       ("$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"),
83        #       rather than same as ("0123456789"*8)[:72]
84        # 255 should be sufficient, but checking
85        (("0123456789"*26)[:254], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
86        (("0123456789"*26)[:255], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
87        (("0123456789"*26)[:256], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
88        (("0123456789"*26)[:257], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
89
90
91        #
92        # from py-bcrypt tests
93        #
94        ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
95        ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'),
96        ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'),
97        ('abcdefghijklmnopqrstuvwxyz',
98                '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
99        ('~!@#$%^&*()      ~!@#$%^&*()PNBFRD',
100                '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'),
101
102        #
103        # custom test vectors
104        #
105
106        # ensures utf-8 used for unicode
107        (UPASS_TABLE,
108                '$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
109
110        # ensure 2b support
111        (UPASS_TABLE,
112                '$2b$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
113
114        ]
115
116    if TEST_MODE("full"):
117        #
118        # add some extra tests related to 2/2a
119        #
120        CONFIG_2 = '$2$05$' + '.'*22
121        CONFIG_A = '$2a$05$' + '.'*22
122        known_correct_hashes.extend([
123            ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
124            ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
125            ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
126            ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'),
127            ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
128            ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'),
129            ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
130            ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
131            ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
132            ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
133        ])
134
135    known_correct_configs = [
136        ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE,
137         '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'),
138    ]
139
140    known_unidentified_hashes = [
141        # invalid minor version
142        "$2f$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
143        "$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
144    ]
145
146    known_malformed_hashes = [
147        # bad char in otherwise correct hash
148        #                 \/
149        "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
150
151        # unsupported (but recognized) minor version
152        "$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
153
154        # rounds not zero-padded (py-bcrypt rejects this, therefore so do we)
155        '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'
156
157        # NOTE: salts with padding bits set are technically malformed,
158        #      but we can reliably correct & issue a warning for that.
159        ]
160
161    platform_crypt_support = [
162        ("freedbsd|openbsd|netbsd", True),
163        ("darwin", False),
164        ("linux", None),  # may be present via addon, e.g. debian's libpam-unix2
165        ("solaris", None),  # depends on system policy
166    ]
167
168    #===================================================================
169    # override some methods
170    #===================================================================
171    def setUp(self):
172        # ensure builtin is enabled for duration of test.
173        if TEST_MODE("full") and self.backend == "builtin":
174            key = "PASSLIB_BUILTIN_BCRYPT"
175            orig = os.environ.get(key)
176            if orig:
177                self.addCleanup(os.environ.__setitem__, key, orig)
178            else:
179                self.addCleanup(os.environ.__delitem__, key)
180            os.environ[key] = "true"
181
182        super(_bcrypt_test, self).setUp()
183
184        # silence this warning, will come up a bunch during testing of old 2a hashes.
185        warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*")
186
187    def populate_settings(self, kwds):
188        # builtin is still just way too slow.
189        if self.backend == "builtin":
190            kwds.setdefault("rounds", 4)
191        super(_bcrypt_test, self).populate_settings(kwds)
192
193    #===================================================================
194    # fuzz testing
195    #===================================================================
196    def crypt_supports_variant(self, hash):
197        """check if OS crypt is expected to support given ident"""
198        from passlib.handlers.bcrypt import bcrypt, IDENT_2X, IDENT_2Y
199        from passlib.utils import safe_crypt
200        ident = bcrypt.from_string(hash)
201        return (safe_crypt("test", ident + "04$5BJqKfqMQvV7nS.yUguNcu") or "").startswith(ident)
202
203    fuzz_verifiers = HandlerCase.fuzz_verifiers + (
204        "fuzz_verifier_bcrypt",
205        "fuzz_verifier_pybcrypt",
206        "fuzz_verifier_bcryptor",
207    )
208
209    def fuzz_verifier_bcrypt(self):
210        # test against bcrypt, if available
211        from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y, _detect_pybcrypt
212        from passlib.utils import to_native_str, to_bytes
213        try:
214            import bcrypt
215        except ImportError:
216            return
217        if _detect_pybcrypt():
218            return
219        def check_bcrypt(secret, hash):
220            """bcrypt"""
221            secret = to_bytes(secret, self.FuzzHashGenerator.password_encoding)
222            if hash.startswith(IDENT_2B):
223                # bcrypt <1.1 lacks 2B support
224                hash = IDENT_2A + hash[4:]
225            elif hash.startswith(IDENT_2):
226                # bcrypt doesn't support $2$ hashes; but we can fake it
227                # using the $2a$ algorithm, by repeating the password until
228                # it's 72 chars in length.
229                hash = IDENT_2A + hash[3:]
230                if secret:
231                    secret = repeat_string(secret, 72)
232            elif hash.startswith(IDENT_2Y) and bcrypt.__version__ == "3.0.0":
233                hash = IDENT_2B + hash[4:]
234            hash = to_bytes(hash)
235            try:
236                return bcrypt.hashpw(secret, hash) == hash
237            except ValueError:
238                raise ValueError("bcrypt rejected hash: %r (secret=%r)" % (hash, secret))
239        return check_bcrypt
240
241    def fuzz_verifier_pybcrypt(self):
242        # test against py-bcrypt, if available
243        from passlib.handlers.bcrypt import (
244            IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y,
245            _PyBcryptBackend,
246        )
247        from passlib.utils import to_native_str
248
249        loaded = _PyBcryptBackend._load_backend_mixin("pybcrypt", False)
250        if not loaded:
251            return
252
253        from passlib.handlers.bcrypt import _pybcrypt as bcrypt_mod
254
255        lock = _PyBcryptBackend._calc_lock # reuse threadlock workaround for pybcrypt 0.2
256
257        def check_pybcrypt(secret, hash):
258            """pybcrypt"""
259            secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding)
260            if len(secret) > 200:  # vulnerable to wraparound bug
261                secret = secret[:200]
262            if hash.startswith((IDENT_2B, IDENT_2Y)):
263                hash = IDENT_2A + hash[4:]
264            try:
265                if lock:
266                    with lock:
267                        return bcrypt_mod.hashpw(secret, hash) == hash
268                else:
269                    return bcrypt_mod.hashpw(secret, hash) == hash
270            except ValueError:
271                raise ValueError("py-bcrypt rejected hash: %r" % (hash,))
272        return check_pybcrypt
273
274    def fuzz_verifier_bcryptor(self):
275        # test against bcryptor if available
276        from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2Y, IDENT_2B
277        from passlib.utils import to_native_str
278        try:
279            from bcryptor.engine import Engine
280        except ImportError:
281            return
282        def check_bcryptor(secret, hash):
283            """bcryptor"""
284            secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding)
285            if hash.startswith((IDENT_2B, IDENT_2Y)):
286                hash = IDENT_2A + hash[4:]
287            elif hash.startswith(IDENT_2):
288                # bcryptor doesn't support $2$ hashes; but we can fake it
289                # using the $2a$ algorithm, by repeating the password until
290                # it's 72 chars in length.
291                hash = IDENT_2A + hash[3:]
292                if secret:
293                    secret = repeat_string(secret, 72)
294            return Engine(False).hash_key(secret, hash) == hash
295        return check_bcryptor
296
297    class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
298
299        def generate(self):
300            opts = super(_bcrypt_test.FuzzHashGenerator, self).generate()
301
302            secret = opts['secret']
303            other = opts['other']
304            settings = opts['settings']
305            ident = settings.get('ident')
306
307            if ident == IDENT_2X:
308                # 2x is just recognized, not supported. don't test with it.
309                del settings['ident']
310
311            elif ident == IDENT_2 and other and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret):
312                # avoid false failure due to flaw in 0-revision bcrypt:
313                # repeated strings like 'abc' and 'abcabc' hash identically.
314                opts['secret'], opts['other'] = self.random_password_pair()
315
316            return opts
317
318        def random_rounds(self):
319            # decrease default rounds for fuzz testing to speed up volume.
320            return self.randintgauss(5, 8, 6, 1)
321
322    #===================================================================
323    # custom tests
324    #===================================================================
325    known_incorrect_padding = [
326        # password, bad hash, good hash
327
328        # 2 bits of salt padding set
329#        ("loppux",                  # \/
330#         "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C",
331#         "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"),
332        ("test",                    # \/
333         '$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO',
334         '$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'),
335
336        # all 4 bits of salt padding set
337#        ("Passlib11",               # \/
338#         "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK",
339#         "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"),
340        ("test",                    # \/
341         "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS",
342         "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
343
344        # bad checksum padding
345        ("test",                                                   # \/
346         "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV",
347         "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
348    ]
349
350    def test_90_bcrypt_padding(self):
351        """test passlib correctly handles bcrypt padding bits"""
352        self.require_TEST_MODE("full")
353        #
354        # prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25)
355        # were some unused bits were incorrectly set in bcrypt salt strings.
356        # (fixed since 1.5.3)
357        #
358        bcrypt = self.handler
359        corr_desc = ".*incorrectly set padding bits"
360
361        #
362        # test hash() / genconfig() don't generate invalid salts anymore
363        #
364        def check_padding(hash):
365            assert hash.startswith(("$2a$", "$2b$")) and len(hash) >= 28, \
366                "unexpectedly malformed hash: %r" % (hash,)
367            self.assertTrue(hash[28] in '.Oeu',
368                            "unused bits incorrectly set in hash: %r" % (hash,))
369        for i in irange(6):
370            check_padding(bcrypt.genconfig())
371        for i in irange(3):
372            check_padding(bcrypt.using(rounds=bcrypt.min_rounds).hash("bob"))
373
374        #
375        # test genconfig() corrects invalid salts & issues warning.
376        #
377        with self.assertWarningList(["salt too large", corr_desc]):
378            hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True)
379        self.assertEqual(hash, "$2b$05$" + "." * (22 + 31))
380
381        #
382        # test public methods against good & bad hashes
383        #
384        samples = self.known_incorrect_padding
385        for pwd, bad, good in samples:
386
387            # make sure genhash() corrects bad configs, leaves good unchanged
388            with self.assertWarningList([corr_desc]):
389                self.assertEqual(bcrypt.genhash(pwd, bad), good)
390            with self.assertWarningList([]):
391                self.assertEqual(bcrypt.genhash(pwd, good), good)
392
393            # make sure verify() works correctly with good & bad hashes
394            with self.assertWarningList([corr_desc]):
395                self.assertTrue(bcrypt.verify(pwd, bad))
396            with self.assertWarningList([]):
397                self.assertTrue(bcrypt.verify(pwd, good))
398
399            # make sure normhash() corrects bad hashes, leaves good unchanged
400            with self.assertWarningList([corr_desc]):
401                self.assertEqual(bcrypt.normhash(bad), good)
402            with self.assertWarningList([]):
403                self.assertEqual(bcrypt.normhash(good), good)
404
405        # make sure normhash() leaves non-bcrypt hashes alone
406        self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc")
407
408    def test_needs_update_w_padding(self):
409        """needs_update corrects bcrypt padding"""
410        # NOTE: see padding test above for details about issue this detects
411        bcrypt = self.handler.using(rounds=4)
412
413        # PASS1 = "test"
414        # bad contains invalid 'c' char at end of salt:
415        #                                    \/
416        BAD1 =  "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
417        GOOD1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
418
419        self.assertTrue(bcrypt.needs_update(BAD1))
420        self.assertFalse(bcrypt.needs_update(GOOD1))
421
422    #===================================================================
423    # eoc
424    #===================================================================
425
426# create test cases for specific backends
427bcrypt_bcrypt_test = _bcrypt_test.create_backend_case("bcrypt")
428bcrypt_pybcrypt_test = _bcrypt_test.create_backend_case("pybcrypt")
429bcrypt_bcryptor_test = _bcrypt_test.create_backend_case("bcryptor")
430
431class bcrypt_os_crypt_test(_bcrypt_test.create_backend_case("os_crypt")):
432
433    # os crypt doesn't support non-utf8 secret bytes
434    known_correct_hashes = [row for row in _bcrypt_test.known_correct_hashes
435                            if is_safe_crypt_input(row[0])]
436
437    # os crypt backend doesn't currently implement a per-call fallback if it fails
438    has_os_crypt_fallback = False
439
440bcrypt_builtin_test = _bcrypt_test.create_backend_case("builtin")
441
442#=============================================================================
443# bcrypt
444#=============================================================================
445class _bcrypt_sha256_test(HandlerCase):
446    "base for BCrypt-SHA256 test cases"
447    handler = hash.bcrypt_sha256
448    reduce_default_rounds = True
449    forbidden_characters = None
450    fuzz_salts_need_bcrypt_repair = True
451
452    known_correct_hashes = [
453        #-------------------------------------------------------------------
454        # custom test vectors for old v1 format
455        #-------------------------------------------------------------------
456
457        # empty
458        ("",
459            '$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'),
460
461        # ascii
462        ("password",
463            '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
464
465        # unicode / utf8
466        (UPASS_TABLE,
467            '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
468        (UPASS_TABLE.encode("utf-8"),
469            '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
470
471        # ensure 2b support
472        ("password",
473            '$bcrypt-sha256$2b,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
474        (UPASS_TABLE,
475            '$bcrypt-sha256$2b,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
476
477        # test >72 chars is hashed correctly -- under bcrypt these hash the same.
478        # NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
479        (repeat_string("abc123", 72),
480            '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'),
481        (repeat_string("abc123", 72) + "qwr",
482            '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'),
483        (repeat_string("abc123", 72) + "xyz",
484            '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'),
485
486        #-------------------------------------------------------------------
487        # custom test vectors for v2 format
488        # TODO: convert to v2 format
489        #-------------------------------------------------------------------
490
491        # empty
492        ("",
493            '$bcrypt-sha256$v=2,t=2b,r=5$E/e/2AOhqM5W/KJTFQzLce$WFPIZKtDDTriqWwlmRFfHiOTeheAZWe'),
494
495        # ascii
496        ("password",
497            '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
498
499        # unicode / utf8
500        (UPASS_TABLE,
501            '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
502        (UPASS_TABLE.encode("utf-8"),
503            '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
504
505        # test >72 chars is hashed correctly -- under bcrypt these hash the same.
506        # NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
507        (repeat_string("abc123", 72),
508            '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zu1cloESVFIOsUIo7fCEgkdHaI9SSue'),
509        (repeat_string("abc123", 72) + "qwr",
510            '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$CBF9csfEdW68xv3DwE6xSULXMtqEFP.'),
511        (repeat_string("abc123", 72) + "xyz",
512            '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zC/1UDUG2ofEXB6Onr2vvyFzfhEOS3S'),
513        ]
514
515    known_correct_configs =[
516        # v1
517        ('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe',
518         "password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
519        # v2
520        ('$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe',
521         "password", '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
522    ]
523
524    known_malformed_hashes = [
525        #-------------------------------------------------------------------
526        # v1 format
527        #-------------------------------------------------------------------
528
529        # bad char in otherwise correct hash
530        #                           \/
531        '$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
532
533        # unrecognized bcrypt variant
534        '$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
535
536        # unsupported bcrypt variant
537        '$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
538
539        # rounds zero-padded
540        '$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
541
542        # config string w/ $ added
543        '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',
544
545        #-------------------------------------------------------------------
546        # v2 format
547        #-------------------------------------------------------------------
548
549        # bad char in otherwise correct hash
550        #                                   \/
551        '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
552
553        # unsupported version (for this format)
554        '$bcrypt-sha256$v=1,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
555
556        # unrecognized version
557        '$bcrypt-sha256$v=3,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
558
559        # unrecognized bcrypt variant
560        '$bcrypt-sha256$v=2,t=2c,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
561
562        # unsupported bcrypt variant
563        '$bcrypt-sha256$v=2,t=2a,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
564        '$bcrypt-sha256$v=2,t=2x,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
565
566        # rounds zero-padded
567        '$bcrypt-sha256$v=2,t=2b,r=05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
568
569        # config string w/ $ added
570        '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$',
571    ]
572
573    #===================================================================
574    # override some methods -- cloned from bcrypt
575    #===================================================================
576    def setUp(self):
577        # ensure builtin is enabled for duration of test.
578        if TEST_MODE("full") and self.backend == "builtin":
579            key = "PASSLIB_BUILTIN_BCRYPT"
580            orig = os.environ.get(key)
581            if orig:
582                self.addCleanup(os.environ.__setitem__, key, orig)
583            else:
584                self.addCleanup(os.environ.__delitem__, key)
585            os.environ[key] = "enabled"
586        super(_bcrypt_sha256_test, self).setUp()
587        warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*")
588
589    def populate_settings(self, kwds):
590        # builtin is still just way too slow.
591        if self.backend == "builtin":
592            kwds.setdefault("rounds", 4)
593        super(_bcrypt_sha256_test, self).populate_settings(kwds)
594
595    #===================================================================
596    # override ident tests for now
597    #===================================================================
598
599    def require_many_idents(self):
600        raise self.skipTest("multiple idents not supported")
601
602    def test_30_HasOneIdent(self):
603        # forbidding ident keyword, we only support "2b" for now
604        handler = self.handler
605        handler(use_defaults=True)
606        self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)
607
608    #===================================================================
609    # fuzz testing -- cloned from bcrypt
610    #===================================================================
611
612    class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
613
614        def random_rounds(self):
615            # decrease default rounds for fuzz testing to speed up volume.
616            return self.randintgauss(5, 8, 6, 1)
617
618        def random_ident(self):
619            return "2b"
620
621    #===================================================================
622    # custom tests
623    #===================================================================
624
625    def test_using_version(self):
626        # default to v2
627        handler = self.handler
628        self.assertEqual(handler.version, 2)
629
630        # allow v1 explicitly
631        subcls = handler.using(version=1)
632        self.assertEqual(subcls.version, 1)
633
634        # forbid unknown ver
635        self.assertRaises(ValueError, handler.using, version=999)
636
637        # allow '2a' only for v1
638        subcls = handler.using(version=1, ident="2a")
639        self.assertRaises(ValueError, handler.using, ident="2a")
640
641    def test_calc_digest_v2(self):
642        """
643        test digest calc v2 matches bcrypt()
644        """
645        from passlib.hash import bcrypt
646        from passlib.crypto.digest import compile_hmac
647        from passlib.utils.binary import b64encode
648
649        # manually calc intermediary digest
650        salt = "nyKYxTAvjmy6lMDYMl11Uu"
651        secret = "test"
652        temp_digest = compile_hmac("sha256", salt.encode("ascii"))(secret.encode("ascii"))
653        temp_digest = b64encode(temp_digest).decode("ascii")
654        self.assertEqual(temp_digest, "J5TlyIDm+IcSWmKiDJm+MeICndBkFVPn4kKdJW8f+xY=")
655
656        # manually final hash from intermediary
657        # XXX: genhash() could be useful here
658        bcrypt_digest = bcrypt(ident="2b", salt=salt, rounds=12)._calc_checksum(temp_digest)
659        self.assertEqual(bcrypt_digest, "M0wE0Ov/9LXoQFCe.jRHu3MSHPF54Ta")
660        self.assertTrue(bcrypt.verify(temp_digest, "$2b$12$" + salt + bcrypt_digest))
661
662        # confirm handler outputs same thing.
663        # XXX: genhash() could be useful here
664        result = self.handler(ident="2b", salt=salt, rounds=12)._calc_checksum(secret)
665        self.assertEqual(result, bcrypt_digest)
666
667    #===================================================================
668    # eoc
669    #===================================================================
670
671# create test cases for specific backends
672bcrypt_sha256_bcrypt_test = _bcrypt_sha256_test.create_backend_case("bcrypt")
673bcrypt_sha256_pybcrypt_test = _bcrypt_sha256_test.create_backend_case("pybcrypt")
674bcrypt_sha256_bcryptor_test = _bcrypt_sha256_test.create_backend_case("bcryptor")
675
676class bcrypt_sha256_os_crypt_test(_bcrypt_sha256_test.create_backend_case("os_crypt")):
677
678    @classmethod
679    def _get_safe_crypt_handler_backend(cls):
680        return bcrypt_os_crypt_test._get_safe_crypt_handler_backend()
681
682    has_os_crypt_fallback = False
683
684bcrypt_sha256_builtin_test = _bcrypt_sha256_test.create_backend_case("builtin")
685
686#=============================================================================
687# eof
688#=============================================================================
689