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