1"""tests for passlib.hash -- (c) Assurance Technologies 2003-2009"""
2#=============================================================================
3# imports
4#=============================================================================
5from __future__ import with_statement
6# core
7import re
8import hashlib
9from logging import getLogger
10import warnings
11# site
12# pkg
13from passlib.hash import ldap_md5, sha256_crypt
14from passlib.exc import MissingBackendError, PasslibHashWarning
15from passlib.utils.compat import str_to_uascii, \
16                                 uascii_to_str, unicode
17import passlib.utils.handlers as uh
18from passlib.tests.utils import HandlerCase, TestCase
19from passlib.utils.compat import u
20# module
21log = getLogger(__name__)
22
23#=============================================================================
24# utils
25#=============================================================================
26def _makelang(alphabet, size):
27    """generate all strings of given size using alphabet"""
28    def helper(size):
29        if size < 2:
30            for char in alphabet:
31                yield char
32        else:
33            for char in alphabet:
34                for tail in helper(size-1):
35                    yield char+tail
36    return set(helper(size))
37
38#=============================================================================
39# test GenericHandler & associates mixin classes
40#=============================================================================
41class SkeletonTest(TestCase):
42    """test hash support classes"""
43
44    #===================================================================
45    # StaticHandler
46    #===================================================================
47    def test_00_static_handler(self):
48        """test StaticHandler class"""
49
50        class d1(uh.StaticHandler):
51            name = "d1"
52            context_kwds = ("flag",)
53            _hash_prefix = u("_")
54            checksum_chars = u("ab")
55            checksum_size = 1
56
57            def __init__(self, flag=False, **kwds):
58                super(d1, self).__init__(**kwds)
59                self.flag = flag
60
61            def _calc_checksum(self, secret):
62                return u('b') if self.flag else u('a')
63
64        # check default identify method
65        self.assertTrue(d1.identify(u('_a')))
66        self.assertTrue(d1.identify(b'_a'))
67        self.assertTrue(d1.identify(u('_b')))
68
69        self.assertFalse(d1.identify(u('_c')))
70        self.assertFalse(d1.identify(b'_c'))
71        self.assertFalse(d1.identify(u('a')))
72        self.assertFalse(d1.identify(u('b')))
73        self.assertFalse(d1.identify(u('c')))
74        self.assertRaises(TypeError, d1.identify, None)
75        self.assertRaises(TypeError, d1.identify, 1)
76
77        # check default genconfig method
78        self.assertEqual(d1.genconfig(), d1.hash(""))
79
80        # check default verify method
81        self.assertTrue(d1.verify('s', b'_a'))
82        self.assertTrue(d1.verify('s',u('_a')))
83        self.assertFalse(d1.verify('s', b'_b'))
84        self.assertFalse(d1.verify('s',u('_b')))
85        self.assertTrue(d1.verify('s', b'_b', flag=True))
86        self.assertRaises(ValueError, d1.verify, 's', b'_c')
87        self.assertRaises(ValueError, d1.verify, 's', u('_c'))
88
89        # check default hash method
90        self.assertEqual(d1.hash('s'), '_a')
91        self.assertEqual(d1.hash('s', flag=True), '_b')
92
93    def test_01_calc_checksum_hack(self):
94        """test StaticHandler legacy attr"""
95        # release 1.5 StaticHandler required genhash(),
96        # not _calc_checksum, be implemented. we have backward compat wrapper,
97        # this tests that it works.
98
99        class d1(uh.StaticHandler):
100            name = "d1"
101
102            @classmethod
103            def identify(cls, hash):
104                if not hash or len(hash) != 40:
105                    return False
106                try:
107                    int(hash, 16)
108                except ValueError:
109                    return False
110                return True
111
112            @classmethod
113            def genhash(cls, secret, hash):
114                if secret is None:
115                    raise TypeError("no secret provided")
116                if isinstance(secret, unicode):
117                    secret = secret.encode("utf-8")
118                # NOTE: have to support hash=None since this is test of legacy 1.5 api
119                if hash is not None and not cls.identify(hash):
120                    raise ValueError("invalid hash")
121                return hashlib.sha1(b"xyz" + secret).hexdigest()
122
123            @classmethod
124            def verify(cls, secret, hash):
125                if hash is None:
126                    raise ValueError("no hash specified")
127                return cls.genhash(secret, hash) == hash.lower()
128
129        # hash should issue api warnings, but everything else should be fine.
130        with self.assertWarningList("d1.*should be updated.*_calc_checksum"):
131            hash = d1.hash("test")
132        self.assertEqual(hash, '7c622762588a0e5cc786ad0a143156f9fd38eea3')
133
134        self.assertTrue(d1.verify("test", hash))
135        self.assertFalse(d1.verify("xtest", hash))
136
137        # not defining genhash either, however, should cause NotImplementedError
138        del d1.genhash
139        self.assertRaises(NotImplementedError, d1.hash, 'test')
140
141    #===================================================================
142    # GenericHandler & mixins
143    #===================================================================
144    def test_10_identify(self):
145        """test GenericHandler.identify()"""
146        class d1(uh.GenericHandler):
147            @classmethod
148            def from_string(cls, hash):
149                if isinstance(hash, bytes):
150                    hash = hash.decode("ascii")
151                if hash == u('a'):
152                    return cls(checksum=hash)
153                else:
154                    raise ValueError
155
156        # check fallback
157        self.assertRaises(TypeError, d1.identify, None)
158        self.assertRaises(TypeError, d1.identify, 1)
159        self.assertFalse(d1.identify(''))
160        self.assertTrue(d1.identify('a'))
161        self.assertFalse(d1.identify('b'))
162
163        # check regexp
164        d1._hash_regex = re.compile(u('@.'))
165        self.assertRaises(TypeError, d1.identify, None)
166        self.assertRaises(TypeError, d1.identify, 1)
167        self.assertTrue(d1.identify('@a'))
168        self.assertFalse(d1.identify('a'))
169        del d1._hash_regex
170
171        # check ident-based
172        d1.ident = u('!')
173        self.assertRaises(TypeError, d1.identify, None)
174        self.assertRaises(TypeError, d1.identify, 1)
175        self.assertTrue(d1.identify('!a'))
176        self.assertFalse(d1.identify('a'))
177        del d1.ident
178
179    def test_11_norm_checksum(self):
180        """test GenericHandler checksum handling"""
181        # setup helpers
182        class d1(uh.GenericHandler):
183            name = 'd1'
184            checksum_size = 4
185            checksum_chars = u('xz')
186
187        def norm_checksum(checksum=None, **k):
188            return d1(checksum=checksum, **k).checksum
189
190        # too small
191        self.assertRaises(ValueError, norm_checksum, u('xxx'))
192
193        # right size
194        self.assertEqual(norm_checksum(u('xxxx')), u('xxxx'))
195        self.assertEqual(norm_checksum(u('xzxz')), u('xzxz'))
196
197        # too large
198        self.assertRaises(ValueError, norm_checksum, u('xxxxx'))
199
200        # wrong chars
201        self.assertRaises(ValueError, norm_checksum, u('xxyx'))
202
203        # wrong type
204        self.assertRaises(TypeError, norm_checksum, b'xxyx')
205
206        # relaxed
207        # NOTE: this could be turned back on if we test _norm_checksum() directly...
208        #with self.assertWarningList("checksum should be unicode"):
209        #    self.assertEqual(norm_checksum(b'xxzx', relaxed=True), u('xxzx'))
210        #self.assertRaises(TypeError, norm_checksum, 1, relaxed=True)
211
212        # test _stub_checksum behavior
213        self.assertEqual(d1()._stub_checksum, u('xxxx'))
214
215    def test_12_norm_checksum_raw(self):
216        """test GenericHandler + HasRawChecksum mixin"""
217        class d1(uh.HasRawChecksum, uh.GenericHandler):
218            name = 'd1'
219            checksum_size = 4
220
221        def norm_checksum(*a, **k):
222            return d1(*a, **k).checksum
223
224        # test bytes
225        self.assertEqual(norm_checksum(b'1234'), b'1234')
226
227        # test unicode
228        self.assertRaises(TypeError, norm_checksum, u('xxyx'))
229
230        # NOTE: this could be turned back on if we test _norm_checksum() directly...
231        # self.assertRaises(TypeError, norm_checksum, u('xxyx'), relaxed=True)
232
233        # test _stub_checksum behavior
234        self.assertEqual(d1()._stub_checksum, b'\x00'*4)
235
236    def test_20_norm_salt(self):
237        """test GenericHandler + HasSalt mixin"""
238        # setup helpers
239        class d1(uh.HasSalt, uh.GenericHandler):
240            name = 'd1'
241            setting_kwds = ('salt',)
242            min_salt_size = 2
243            max_salt_size = 4
244            default_salt_size = 3
245            salt_chars = 'ab'
246
247        def norm_salt(**k):
248            return d1(**k).salt
249
250        def gen_salt(sz, **k):
251            return d1.using(salt_size=sz, **k)(use_defaults=True).salt
252
253        salts2 = _makelang('ab', 2)
254        salts3 = _makelang('ab', 3)
255        salts4 = _makelang('ab', 4)
256
257        # check salt=None
258        self.assertRaises(TypeError, norm_salt)
259        self.assertRaises(TypeError, norm_salt, salt=None)
260        self.assertIn(norm_salt(use_defaults=True), salts3)
261
262        # check explicit salts
263        with warnings.catch_warnings(record=True) as wlog:
264
265            # check too-small salts
266            self.assertRaises(ValueError, norm_salt, salt='')
267            self.assertRaises(ValueError, norm_salt, salt='a')
268            self.consumeWarningList(wlog)
269
270            # check correct salts
271            self.assertEqual(norm_salt(salt='ab'), 'ab')
272            self.assertEqual(norm_salt(salt='aba'), 'aba')
273            self.assertEqual(norm_salt(salt='abba'), 'abba')
274            self.consumeWarningList(wlog)
275
276            # check too-large salts
277            self.assertRaises(ValueError, norm_salt, salt='aaaabb')
278            self.consumeWarningList(wlog)
279
280        # check generated salts
281        with warnings.catch_warnings(record=True) as wlog:
282
283            # check too-small salt size
284            self.assertRaises(ValueError, gen_salt, 0)
285            self.assertRaises(ValueError, gen_salt, 1)
286            self.consumeWarningList(wlog)
287
288            # check correct salt size
289            self.assertIn(gen_salt(2), salts2)
290            self.assertIn(gen_salt(3), salts3)
291            self.assertIn(gen_salt(4), salts4)
292            self.consumeWarningList(wlog)
293
294            # check too-large salt size
295            self.assertRaises(ValueError, gen_salt, 5)
296            self.consumeWarningList(wlog)
297
298            self.assertIn(gen_salt(5, relaxed=True), salts4)
299            self.consumeWarningList(wlog, ["salt_size.*above max_salt_size"])
300
301        # test with max_salt_size=None
302        del d1.max_salt_size
303        with self.assertWarningList([]):
304            self.assertEqual(len(gen_salt(None)), 3)
305            self.assertEqual(len(gen_salt(5)), 5)
306
307    # TODO: test HasRawSalt mixin
308
309    def test_30_init_rounds(self):
310        """test GenericHandler + HasRounds mixin"""
311        # setup helpers
312        class d1(uh.HasRounds, uh.GenericHandler):
313            name = 'd1'
314            setting_kwds = ('rounds',)
315            min_rounds = 1
316            max_rounds = 3
317            default_rounds = 2
318
319        # NOTE: really is testing _init_rounds(), could dup to test _norm_rounds() via .replace
320        def norm_rounds(**k):
321            return d1(**k).rounds
322
323        # check rounds=None
324        self.assertRaises(TypeError, norm_rounds)
325        self.assertRaises(TypeError, norm_rounds, rounds=None)
326        self.assertEqual(norm_rounds(use_defaults=True), 2)
327
328        # check rounds=non int
329        self.assertRaises(TypeError, norm_rounds, rounds=1.5)
330
331        # check explicit rounds
332        with warnings.catch_warnings(record=True) as wlog:
333            # too small
334            self.assertRaises(ValueError, norm_rounds, rounds=0)
335            self.consumeWarningList(wlog)
336
337            # just right
338            self.assertEqual(norm_rounds(rounds=1), 1)
339            self.assertEqual(norm_rounds(rounds=2), 2)
340            self.assertEqual(norm_rounds(rounds=3), 3)
341            self.consumeWarningList(wlog)
342
343            # too large
344            self.assertRaises(ValueError, norm_rounds, rounds=4)
345            self.consumeWarningList(wlog)
346
347        # check no default rounds
348        d1.default_rounds = None
349        self.assertRaises(TypeError, norm_rounds, use_defaults=True)
350
351    def test_40_backends(self):
352        """test GenericHandler + HasManyBackends mixin"""
353        class d1(uh.HasManyBackends, uh.GenericHandler):
354            name = 'd1'
355            setting_kwds = ()
356
357            backends = ("a", "b")
358
359            _enable_a = False
360            _enable_b = False
361
362            @classmethod
363            def _load_backend_a(cls):
364                if cls._enable_a:
365                    cls._set_calc_checksum_backend(cls._calc_checksum_a)
366                    return True
367                else:
368                    return False
369
370            @classmethod
371            def _load_backend_b(cls):
372                if cls._enable_b:
373                    cls._set_calc_checksum_backend(cls._calc_checksum_b)
374                    return True
375                else:
376                    return False
377
378            def _calc_checksum_a(self, secret):
379                return 'a'
380
381            def _calc_checksum_b(self, secret):
382                return 'b'
383
384        # test no backends
385        self.assertRaises(MissingBackendError, d1.get_backend)
386        self.assertRaises(MissingBackendError, d1.set_backend)
387        self.assertRaises(MissingBackendError, d1.set_backend, 'any')
388        self.assertRaises(MissingBackendError, d1.set_backend, 'default')
389        self.assertFalse(d1.has_backend())
390
391        # enable 'b' backend
392        d1._enable_b = True
393
394        # test lazy load
395        obj = d1()
396        self.assertEqual(obj._calc_checksum('s'), 'b')
397
398        # test repeat load
399        d1.set_backend('b')
400        d1.set_backend('any')
401        self.assertEqual(obj._calc_checksum('s'), 'b')
402
403        # test unavailable
404        self.assertRaises(MissingBackendError, d1.set_backend, 'a')
405        self.assertTrue(d1.has_backend('b'))
406        self.assertFalse(d1.has_backend('a'))
407
408        # enable 'a' backend also
409        d1._enable_a = True
410
411        # test explicit
412        self.assertTrue(d1.has_backend())
413        d1.set_backend('a')
414        self.assertEqual(obj._calc_checksum('s'), 'a')
415
416        # test unknown backend
417        self.assertRaises(ValueError, d1.set_backend, 'c')
418        self.assertRaises(ValueError, d1.has_backend, 'c')
419
420        # test error thrown if _has & _load are mixed
421        d1.set_backend("b")  # switch away from 'a' so next call actually checks loader
422        class d2(d1):
423            _has_backend_a = True
424        self.assertRaises(AssertionError, d2.has_backend, "a")
425
426    def test_41_backends(self):
427        """test GenericHandler + HasManyBackends mixin (deprecated api)"""
428        warnings.filterwarnings("ignore",
429            category=DeprecationWarning,
430            message=r".* support for \._has_backend_.* is deprecated.*",
431            )
432
433        class d1(uh.HasManyBackends, uh.GenericHandler):
434            name = 'd1'
435            setting_kwds = ()
436
437            backends = ("a", "b")
438
439            _has_backend_a = False
440            _has_backend_b = False
441
442            def _calc_checksum_a(self, secret):
443                return 'a'
444
445            def _calc_checksum_b(self, secret):
446                return 'b'
447
448        # test no backends
449        self.assertRaises(MissingBackendError, d1.get_backend)
450        self.assertRaises(MissingBackendError, d1.set_backend)
451        self.assertRaises(MissingBackendError, d1.set_backend, 'any')
452        self.assertRaises(MissingBackendError, d1.set_backend, 'default')
453        self.assertFalse(d1.has_backend())
454
455        # enable 'b' backend
456        d1._has_backend_b = True
457
458        # test lazy load
459        obj = d1()
460        self.assertEqual(obj._calc_checksum('s'), 'b')
461
462        # test repeat load
463        d1.set_backend('b')
464        d1.set_backend('any')
465        self.assertEqual(obj._calc_checksum('s'), 'b')
466
467        # test unavailable
468        self.assertRaises(MissingBackendError, d1.set_backend, 'a')
469        self.assertTrue(d1.has_backend('b'))
470        self.assertFalse(d1.has_backend('a'))
471
472        # enable 'a' backend also
473        d1._has_backend_a = True
474
475        # test explicit
476        self.assertTrue(d1.has_backend())
477        d1.set_backend('a')
478        self.assertEqual(obj._calc_checksum('s'), 'a')
479
480        # test unknown backend
481        self.assertRaises(ValueError, d1.set_backend, 'c')
482        self.assertRaises(ValueError, d1.has_backend, 'c')
483
484    def test_50_norm_ident(self):
485        """test GenericHandler + HasManyIdents"""
486        # setup helpers
487        class d1(uh.HasManyIdents, uh.GenericHandler):
488            name = 'd1'
489            setting_kwds = ('ident',)
490            default_ident = u("!A")
491            ident_values = (u("!A"), u("!B"))
492            ident_aliases = { u("A"): u("!A")}
493
494        def norm_ident(**k):
495            return d1(**k).ident
496
497        # check ident=None
498        self.assertRaises(TypeError, norm_ident)
499        self.assertRaises(TypeError, norm_ident, ident=None)
500        self.assertEqual(norm_ident(use_defaults=True), u('!A'))
501
502        # check valid idents
503        self.assertEqual(norm_ident(ident=u('!A')), u('!A'))
504        self.assertEqual(norm_ident(ident=u('!B')), u('!B'))
505        self.assertRaises(ValueError, norm_ident, ident=u('!C'))
506
507        # check aliases
508        self.assertEqual(norm_ident(ident=u('A')), u('!A'))
509
510        # check invalid idents
511        self.assertRaises(ValueError, norm_ident, ident=u('B'))
512
513        # check identify is honoring ident system
514        self.assertTrue(d1.identify(u("!Axxx")))
515        self.assertTrue(d1.identify(u("!Bxxx")))
516        self.assertFalse(d1.identify(u("!Cxxx")))
517        self.assertFalse(d1.identify(u("A")))
518        self.assertFalse(d1.identify(u("")))
519        self.assertRaises(TypeError, d1.identify, None)
520        self.assertRaises(TypeError, d1.identify, 1)
521
522        # check default_ident missing is detected.
523        d1.default_ident = None
524        self.assertRaises(AssertionError, norm_ident, use_defaults=True)
525
526    #===================================================================
527    # experimental - the following methods are not finished or tested,
528    # but way work correctly for some hashes
529    #===================================================================
530    def test_91_parsehash(self):
531        """test parsehash()"""
532        # NOTE: this just tests some existing GenericHandler classes
533        from passlib import hash
534
535        #
536        # parsehash()
537        #
538
539        # simple hash w/ salt
540        result = hash.des_crypt.parsehash("OgAwTx2l6NADI")
541        self.assertEqual(result, {'checksum': u('AwTx2l6NADI'), 'salt': u('Og')})
542
543        # parse rounds and extra implicit_rounds flag
544        h = '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9'
545        s = u('LKO/Ute40T3FNF95')
546        c = u('U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9')
547        result = hash.sha256_crypt.parsehash(h)
548        self.assertEqual(result, dict(salt=s, rounds=5000,
549                                      implicit_rounds=True, checksum=c))
550
551        # omit checksum
552        result = hash.sha256_crypt.parsehash(h, checksum=False)
553        self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True))
554
555        # sanitize
556        result = hash.sha256_crypt.parsehash(h, sanitize=True)
557        self.assertEqual(result, dict(rounds=5000, implicit_rounds=True,
558            salt=u('LK**************'),
559             checksum=u('U0pr***************************************')))
560
561        # parse w/o implicit rounds flag
562        result = hash.sha256_crypt.parsehash('$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3')
563        self.assertEqual(result, dict(
564            checksum=u('YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
565            salt=u('uy/jIAhCetNCTtb0'),
566            rounds=10428,
567        ))
568
569        # parsing of raw checksums & salts
570        h1 = '$pbkdf2$60000$DoEwpvQeA8B4T.k951yLUQ$O26Y3/NJEiLCVaOVPxGXshyjW8k'
571        result = hash.pbkdf2_sha1.parsehash(h1)
572        self.assertEqual(result, dict(
573            checksum=b';n\x98\xdf\xf3I\x12"\xc2U\xa3\x95?\x11\x97\xb2\x1c\xa3[\xc9',
574            rounds=60000,
575            salt=b'\x0e\x810\xa6\xf4\x1e\x03\xc0xO\xe9=\xe7\\\x8bQ',
576        ))
577
578        # sanitizing of raw checksums & salts
579        result = hash.pbkdf2_sha1.parsehash(h1, sanitize=True)
580        self.assertEqual(result, dict(
581            checksum=u('O26************************'),
582            rounds=60000,
583            salt=u('Do********************'),
584        ))
585
586    def test_92_bitsize(self):
587        """test bitsize()"""
588        # NOTE: this just tests some existing GenericHandler classes
589        from passlib import hash
590
591        # no rounds
592        self.assertEqual(hash.des_crypt.bitsize(),
593                         {'checksum': 66, 'salt': 12})
594
595        # log2 rounds
596        self.assertEqual(hash.bcrypt.bitsize(),
597                         {'checksum': 186, 'salt': 132})
598
599        # linear rounds
600        # NOTE: +3 comes from int(math.log(.1,2)),
601        #       where 0.1 = 10% = default allowed variation in rounds
602        self.patchAttr(hash.sha256_crypt, "default_rounds", 1 << (14 + 3))
603        self.assertEqual(hash.sha256_crypt.bitsize(),
604                         {'checksum': 258, 'rounds': 14, 'salt': 96})
605
606        # raw checksum
607        self.patchAttr(hash.pbkdf2_sha1, "default_rounds", 1 << (13 + 3))
608        self.assertEqual(hash.pbkdf2_sha1.bitsize(),
609                         {'checksum': 160, 'rounds': 13, 'salt': 128})
610
611        # TODO: handle fshp correctly, and other glitches noted in code.
612        ##self.assertEqual(hash.fshp.bitsize(variant=1),
613        ##                {'checksum': 256, 'rounds': 13, 'salt': 128})
614
615    #===================================================================
616    # eoc
617    #===================================================================
618
619#=============================================================================
620# PrefixWrapper
621#=============================================================================
622class dummy_handler_in_registry(object):
623    """context manager that inserts dummy handler in registry"""
624    def __init__(self, name):
625        self.name = name
626        self.dummy = type('dummy_' + name, (uh.GenericHandler,), dict(
627            name=name,
628            setting_kwds=(),
629        ))
630
631    def __enter__(self):
632        from passlib import registry
633        registry._unload_handler_name(self.name, locations=False)
634        registry.register_crypt_handler(self.dummy)
635        assert registry.get_crypt_handler(self.name) is self.dummy
636        return self.dummy
637
638    def __exit__(self, *exc_info):
639        from passlib import registry
640        registry._unload_handler_name(self.name, locations=False)
641
642class PrefixWrapperTest(TestCase):
643    """test PrefixWrapper class"""
644
645    def test_00_lazy_loading(self):
646        """test PrefixWrapper lazy loading of handler"""
647        d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}", lazy=True)
648
649        # check base state
650        self.assertEqual(d1._wrapped_name, "ldap_md5")
651        self.assertIs(d1._wrapped_handler, None)
652
653        # check loading works
654        self.assertIs(d1.wrapped, ldap_md5)
655        self.assertIs(d1._wrapped_handler, ldap_md5)
656
657        # replace w/ wrong handler, make sure doesn't reload w/ dummy
658        with dummy_handler_in_registry("ldap_md5") as dummy:
659            self.assertIs(d1.wrapped, ldap_md5)
660
661    def test_01_active_loading(self):
662        """test PrefixWrapper active loading of handler"""
663        d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
664
665        # check base state
666        self.assertEqual(d1._wrapped_name, "ldap_md5")
667        self.assertIs(d1._wrapped_handler, ldap_md5)
668        self.assertIs(d1.wrapped, ldap_md5)
669
670        # replace w/ wrong handler, make sure doesn't reload w/ dummy
671        with dummy_handler_in_registry("ldap_md5") as dummy:
672            self.assertIs(d1.wrapped, ldap_md5)
673
674    def test_02_explicit(self):
675        """test PrefixWrapper with explicitly specified handler"""
676
677        d1 = uh.PrefixWrapper("d1", ldap_md5, "{XXX}", "{MD5}")
678
679        # check base state
680        self.assertEqual(d1._wrapped_name, None)
681        self.assertIs(d1._wrapped_handler, ldap_md5)
682        self.assertIs(d1.wrapped, ldap_md5)
683
684        # replace w/ wrong handler, make sure doesn't reload w/ dummy
685        with dummy_handler_in_registry("ldap_md5") as dummy:
686            self.assertIs(d1.wrapped, ldap_md5)
687
688    def test_10_wrapped_attributes(self):
689        d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
690        self.assertEqual(d1.name, "d1")
691        self.assertIs(d1.setting_kwds, ldap_md5.setting_kwds)
692        self.assertFalse('max_rounds' in dir(d1))
693
694        d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}")
695        self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds)
696        self.assertTrue('max_rounds' in dir(d2))
697
698    def test_11_wrapped_methods(self):
699        d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
700        dph = "{XXX}X03MO1qnZdYdgyfeuILPmQ=="
701        lph = "{MD5}X03MO1qnZdYdgyfeuILPmQ=="
702
703        # genconfig
704        self.assertEqual(d1.genconfig(), '{XXX}1B2M2Y8AsgTpgAmY7PhCfg==')
705
706        # genhash
707        self.assertRaises(TypeError, d1.genhash, "password", None)
708        self.assertEqual(d1.genhash("password", dph), dph)
709        self.assertRaises(ValueError, d1.genhash, "password", lph)
710
711        # hash
712        self.assertEqual(d1.hash("password"), dph)
713
714        # identify
715        self.assertTrue(d1.identify(dph))
716        self.assertFalse(d1.identify(lph))
717
718        # verify
719        self.assertRaises(ValueError, d1.verify, "password", lph)
720        self.assertTrue(d1.verify("password", dph))
721
722    def test_12_ident(self):
723        # test ident is proxied
724        h = uh.PrefixWrapper("h2", "ldap_md5", "{XXX}")
725        self.assertEqual(h.ident, u("{XXX}{MD5}"))
726        self.assertIs(h.ident_values, None)
727
728        # test lack of ident means no proxy
729        h = uh.PrefixWrapper("h2", "des_crypt", "{XXX}")
730        self.assertIs(h.ident, None)
731        self.assertIs(h.ident_values, None)
732
733        # test orig_prefix disabled ident proxy
734        h = uh.PrefixWrapper("h1", "ldap_md5", "{XXX}", "{MD5}")
735        self.assertIs(h.ident, None)
736        self.assertIs(h.ident_values, None)
737
738        # test custom ident overrides default
739        h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{X")
740        self.assertEqual(h.ident, u("{X"))
741        self.assertIs(h.ident_values, None)
742
743        # test custom ident must match
744        h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{XXX}A")
745        self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5",
746                          "{XXX}", ident="{XY")
747        self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5",
748                          "{XXX}", ident="{XXXX")
749
750        # test ident_values is proxied
751        h = uh.PrefixWrapper("h4", "phpass", "{XXX}")
752        self.assertIs(h.ident, None)
753        self.assertEqual(h.ident_values, (u("{XXX}$P$"), u("{XXX}$H$")))
754
755        # test ident=True means use prefix even if hash has no ident.
756        h = uh.PrefixWrapper("h5", "des_crypt", "{XXX}", ident=True)
757        self.assertEqual(h.ident, u("{XXX}"))
758        self.assertIs(h.ident_values, None)
759
760        # ... but requires prefix
761        self.assertRaises(ValueError, uh.PrefixWrapper, "h6", "des_crypt", ident=True)
762
763        # orig_prefix + HasManyIdent - warning
764        with self.assertWarningList("orig_prefix.*may not work correctly"):
765            h = uh.PrefixWrapper("h7", "phpass", orig_prefix="$", prefix="?")
766        self.assertEqual(h.ident_values, None) # TODO: should output (u("?P$"), u("?H$")))
767        self.assertEqual(h.ident, None)
768
769    def test_13_repr(self):
770        """test repr()"""
771        h = uh.PrefixWrapper("h2", "md5_crypt", "{XXX}", orig_prefix="$1$")
772        self.assertRegex(repr(h),
773            r"""(?x)^PrefixWrapper\(
774                ['"]h2['"],\s+
775                ['"]md5_crypt['"],\s+
776                prefix=u?["']{XXX}['"],\s+
777                orig_prefix=u?["']\$1\$['"]
778            \)$""")
779
780    def test_14_bad_hash(self):
781        """test orig_prefix sanity check"""
782        # shoudl throw InvalidHashError if wrapped hash doesn't begin
783        # with orig_prefix.
784        h = uh.PrefixWrapper("h2", "md5_crypt", orig_prefix="$6$")
785        self.assertRaises(ValueError, h.hash, 'test')
786
787#=============================================================================
788# sample algorithms - these serve as known quantities
789# to test the unittests themselves, as well as other
790# parts of passlib. they shouldn't be used as actual password schemes.
791#=============================================================================
792class UnsaltedHash(uh.StaticHandler):
793    """test algorithm which lacks a salt"""
794    name = "unsalted_test_hash"
795    checksum_chars = uh.LOWER_HEX_CHARS
796    checksum_size = 40
797
798    def _calc_checksum(self, secret):
799        if isinstance(secret, unicode):
800            secret = secret.encode("utf-8")
801        data = b"boblious" + secret
802        return str_to_uascii(hashlib.sha1(data).hexdigest())
803
804class SaltedHash(uh.HasSalt, uh.GenericHandler):
805    """test algorithm with a salt"""
806    name = "salted_test_hash"
807    setting_kwds = ("salt",)
808
809    min_salt_size = 2
810    max_salt_size = 4
811    checksum_size = 40
812    salt_chars = checksum_chars = uh.LOWER_HEX_CHARS
813
814    _hash_regex = re.compile(u("^@salt[0-9a-f]{42,44}$"))
815
816    @classmethod
817    def from_string(cls, hash):
818        if not cls.identify(hash):
819            raise uh.exc.InvalidHashError(cls)
820        if isinstance(hash, bytes):
821            hash = hash.decode("ascii")
822        return cls(salt=hash[5:-40], checksum=hash[-40:])
823
824    def to_string(self):
825        hash = u("@salt%s%s") % (self.salt, self.checksum)
826        return uascii_to_str(hash)
827
828    def _calc_checksum(self, secret):
829        if isinstance(secret, unicode):
830            secret = secret.encode("utf-8")
831        data = self.salt.encode("ascii") + secret + self.salt.encode("ascii")
832        return str_to_uascii(hashlib.sha1(data).hexdigest())
833
834#=============================================================================
835# test sample algorithms - really a self-test of HandlerCase
836#=============================================================================
837
838# TODO: provide data samples for algorithms
839#       (positive knowns, negative knowns, invalid identify)
840
841UPASS_TEMP = u('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2')
842
843class UnsaltedHashTest(HandlerCase):
844    handler = UnsaltedHash
845
846    known_correct_hashes = [
847        ("password", "61cfd32684c47de231f1f982c214e884133762c0"),
848        (UPASS_TEMP, '96b329d120b97ff81ada770042e44ba87343ad2b'),
849    ]
850
851    def test_bad_kwds(self):
852        self.assertRaises(TypeError, UnsaltedHash, salt='x')
853        self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1)
854
855class SaltedHashTest(HandlerCase):
856    handler = SaltedHash
857
858    known_correct_hashes = [
859        ("password", '@salt77d71f8fe74f314dac946766c1ac4a2a58365482c0'),
860        (UPASS_TEMP, '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'),
861    ]
862
863    def test_bad_kwds(self):
864        stub = SaltedHash(use_defaults=True)._stub_checksum
865        self.assertRaises(TypeError, SaltedHash, checksum=stub, salt=None)
866        self.assertRaises(ValueError, SaltedHash, checksum=stub, salt='xxx')
867
868#=============================================================================
869# eof
870#=============================================================================
871