You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

3622 line
144 KiB

  1. """helpers for passlib unittests"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import with_statement
  6. # core
  7. from binascii import unhexlify
  8. import contextlib
  9. from functools import wraps, partial
  10. import hashlib
  11. import logging; log = logging.getLogger(__name__)
  12. import random
  13. import re
  14. import os
  15. import sys
  16. import tempfile
  17. import threading
  18. import time
  19. from passlib.exc import PasslibHashWarning, PasslibConfigWarning
  20. from passlib.utils.compat import PY3, JYTHON
  21. import warnings
  22. from warnings import warn
  23. # site
  24. # pkg
  25. from passlib import exc
  26. from passlib.exc import MissingBackendError
  27. import passlib.registry as registry
  28. from passlib.tests.backports import TestCase as _TestCase, skip, skipIf, skipUnless, SkipTest
  29. from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \
  30. rng as sys_rng, getrandstr, is_ascii_safe, to_native_str, \
  31. repeat_string, tick, batch
  32. from passlib.utils.compat import iteritems, irange, u, unicode, PY2, nullcontext
  33. from passlib.utils.decor import classproperty
  34. import passlib.utils.handlers as uh
  35. # local
  36. __all__ = [
  37. # util funcs
  38. 'TEST_MODE',
  39. 'set_file', 'get_file',
  40. # unit testing
  41. 'TestCase',
  42. 'HandlerCase',
  43. ]
  44. #=============================================================================
  45. # environment detection
  46. #=============================================================================
  47. # figure out if we're running under GAE;
  48. # some tests (e.g. FS writing) should be skipped.
  49. # XXX: is there better way to do this?
  50. try:
  51. import google.appengine
  52. except ImportError:
  53. GAE = False
  54. else:
  55. GAE = True
  56. def ensure_mtime_changed(path):
  57. """ensure file's mtime has changed"""
  58. # NOTE: this is hack to deal w/ filesystems whose mtime resolution is >= 1s,
  59. # when a test needs to be sure the mtime changed after writing to the file.
  60. last = os.path.getmtime(path)
  61. while os.path.getmtime(path) == last:
  62. time.sleep(0.1)
  63. os.utime(path, None)
  64. def _get_timer_resolution(timer):
  65. def sample():
  66. start = cur = timer()
  67. while start == cur:
  68. cur = timer()
  69. return cur-start
  70. return min(sample() for _ in range(3))
  71. TICK_RESOLUTION = _get_timer_resolution(tick)
  72. #=============================================================================
  73. # test mode
  74. #=============================================================================
  75. _TEST_MODES = ["quick", "default", "full"]
  76. _test_mode = _TEST_MODES.index(os.environ.get("PASSLIB_TEST_MODE",
  77. "default").strip().lower())
  78. def TEST_MODE(min=None, max=None):
  79. """check if test for specified mode should be enabled.
  80. ``"quick"``
  81. run the bare minimum tests to ensure functionality.
  82. variable-cost hashes are tested at their lowest setting.
  83. hash algorithms are only tested against the backend that will
  84. be used on the current host. no fuzz testing is done.
  85. ``"default"``
  86. same as ``"quick"``, except: hash algorithms are tested
  87. at default levels, and a brief round of fuzz testing is done
  88. for each hash.
  89. ``"full"``
  90. extra regression and internal tests are enabled, hash algorithms are tested
  91. against all available backends, unavailable ones are mocked whre possible,
  92. additional time is devoted to fuzz testing.
  93. """
  94. if min and _test_mode < _TEST_MODES.index(min):
  95. return False
  96. if max and _test_mode > _TEST_MODES.index(max):
  97. return False
  98. return True
  99. #=============================================================================
  100. # hash object inspection
  101. #=============================================================================
  102. def has_relaxed_setting(handler):
  103. """check if handler supports 'relaxed' kwd"""
  104. # FIXME: I've been lazy, should probably just add 'relaxed' kwd
  105. # to all handlers that derive from GenericHandler
  106. # ignore wrapper classes for now.. though could introspec.
  107. if hasattr(handler, "orig_prefix"):
  108. return False
  109. return 'relaxed' in handler.setting_kwds or issubclass(handler,
  110. uh.GenericHandler)
  111. def get_effective_rounds(handler, rounds=None):
  112. """get effective rounds value from handler"""
  113. handler = unwrap_handler(handler)
  114. return handler(rounds=rounds, use_defaults=True).rounds
  115. def is_default_backend(handler, backend):
  116. """check if backend is the default for source"""
  117. try:
  118. orig = handler.get_backend()
  119. except MissingBackendError:
  120. return False
  121. try:
  122. handler.set_backend("default")
  123. return handler.get_backend() == backend
  124. finally:
  125. handler.set_backend(orig)
  126. def iter_alt_backends(handler, current=None, fallback=False):
  127. """
  128. iterate over alternate backends available to handler.
  129. .. warning::
  130. not thread-safe due to has_backend() call
  131. """
  132. if current is None:
  133. current = handler.get_backend()
  134. backends = handler.backends
  135. idx = backends.index(current)+1 if fallback else 0
  136. for backend in backends[idx:]:
  137. if backend != current and handler.has_backend(backend):
  138. yield backend
  139. def get_alt_backend(*args, **kwds):
  140. for backend in iter_alt_backends(*args, **kwds):
  141. return backend
  142. return None
  143. def unwrap_handler(handler):
  144. """return original handler, removing any wrapper objects"""
  145. while hasattr(handler, "wrapped"):
  146. handler = handler.wrapped
  147. return handler
  148. def handler_derived_from(handler, base):
  149. """
  150. test if <handler> was derived from <base> via <base.using()>.
  151. """
  152. # XXX: need way to do this more formally via ifc,
  153. # for now just hacking in the cases we encounter in testing.
  154. if handler == base:
  155. return True
  156. elif isinstance(handler, uh.PrefixWrapper):
  157. while handler:
  158. if handler == base:
  159. return True
  160. # helper set by PrefixWrapper().using() just for this case...
  161. handler = handler._derived_from
  162. return False
  163. elif isinstance(handler, type) and issubclass(handler, uh.MinimalHandler):
  164. return issubclass(handler, base)
  165. else:
  166. raise NotImplementedError("don't know how to inspect handler: %r" % (handler,))
  167. @contextlib.contextmanager
  168. def patch_calc_min_rounds(handler):
  169. """
  170. internal helper for do_config_encrypt() --
  171. context manager which temporarily replaces handler's _calc_checksum()
  172. with one that uses min_rounds; useful when trying to generate config
  173. with high rounds value, but don't care if output is correct.
  174. """
  175. if isinstance(handler, type) and issubclass(handler, uh.HasRounds):
  176. # XXX: also require GenericHandler for this branch?
  177. wrapped = handler._calc_checksum
  178. def wrapper(self, *args, **kwds):
  179. rounds = self.rounds
  180. try:
  181. self.rounds = self.min_rounds
  182. return wrapped(self, *args, **kwds)
  183. finally:
  184. self.rounds = rounds
  185. handler._calc_checksum = wrapper
  186. try:
  187. yield
  188. finally:
  189. handler._calc_checksum = wrapped
  190. elif isinstance(handler, uh.PrefixWrapper):
  191. with patch_calc_min_rounds(handler.wrapped):
  192. yield
  193. else:
  194. yield
  195. return
  196. #=============================================================================
  197. # misc helpers
  198. #=============================================================================
  199. def set_file(path, content):
  200. """set file to specified bytes"""
  201. if isinstance(content, unicode):
  202. content = content.encode("utf-8")
  203. with open(path, "wb") as fh:
  204. fh.write(content)
  205. def get_file(path):
  206. """read file as bytes"""
  207. with open(path, "rb") as fh:
  208. return fh.read()
  209. def tonn(source):
  210. """convert native string to non-native string"""
  211. if not isinstance(source, str):
  212. return source
  213. elif PY3:
  214. return source.encode("utf-8")
  215. else:
  216. try:
  217. return source.decode("utf-8")
  218. except UnicodeDecodeError:
  219. return source.decode("latin-1")
  220. def hb(source):
  221. """
  222. helper for represent byte strings in hex.
  223. usage: ``hb("deadbeef23")``
  224. """
  225. return unhexlify(re.sub(r"\s", "", source))
  226. def limit(value, lower, upper):
  227. if value < lower:
  228. return lower
  229. elif value > upper:
  230. return upper
  231. return value
  232. def quicksleep(delay):
  233. """because time.sleep() doesn't even have 10ms accuracy on some OSes"""
  234. start = tick()
  235. while tick()-start < delay:
  236. pass
  237. def time_call(func, setup=None, maxtime=1, bestof=10):
  238. """
  239. timeit() wrapper which tries to get as accurate a measurement as possible w/in maxtime seconds.
  240. :returns:
  241. ``(avg_seconds_per_call, log10_number_of_repetitions)``
  242. """
  243. from timeit import Timer
  244. from math import log
  245. timer = Timer(func, setup=setup or '')
  246. number = 1
  247. end = tick() + maxtime
  248. while True:
  249. delta = min(timer.repeat(bestof, number))
  250. if tick() >= end:
  251. return delta/number, int(log(number, 10))
  252. number *= 10
  253. def run_with_fixed_seeds(count=128, master_seed=0x243F6A8885A308D3):
  254. """
  255. decorator run test method w/ multiple fixed seeds.
  256. """
  257. def builder(func):
  258. @wraps(func)
  259. def wrapper(*args, **kwds):
  260. rng = random.Random(master_seed)
  261. for _ in irange(count):
  262. kwds['seed'] = rng.getrandbits(32)
  263. func(*args, **kwds)
  264. return wrapper
  265. return builder
  266. #=============================================================================
  267. # custom test harness
  268. #=============================================================================
  269. class TestCase(_TestCase):
  270. """passlib-specific test case class
  271. this class adds a number of features to the standard TestCase...
  272. * common prefix for all test descriptions
  273. * resets warnings filter & registry for every test
  274. * tweaks to message formatting
  275. * __msg__ kwd added to assertRaises()
  276. * suite of methods for matching against warnings
  277. """
  278. #===================================================================
  279. # add various custom features
  280. #===================================================================
  281. #---------------------------------------------------------------
  282. # make it easy for test cases to add common prefix to shortDescription
  283. #---------------------------------------------------------------
  284. # string prepended to all tests in TestCase
  285. descriptionPrefix = None
  286. def shortDescription(self):
  287. """wrap shortDescription() method to prepend descriptionPrefix"""
  288. desc = super(TestCase, self).shortDescription()
  289. prefix = self.descriptionPrefix
  290. if prefix:
  291. desc = "%s: %s" % (prefix, desc or str(self))
  292. return desc
  293. #---------------------------------------------------------------
  294. # hack things so nose and ut2 both skip subclasses who have
  295. # "__unittest_skip=True" set, or whose names start with "_"
  296. #---------------------------------------------------------------
  297. @classproperty
  298. def __unittest_skip__(cls):
  299. # NOTE: this attr is technically a unittest2 internal detail.
  300. name = cls.__name__
  301. return name.startswith("_") or \
  302. getattr(cls, "_%s__unittest_skip" % name, False)
  303. @classproperty
  304. def __test__(cls):
  305. # make nose just proxy __unittest_skip__
  306. return not cls.__unittest_skip__
  307. # flag to skip *this* class
  308. __unittest_skip = True
  309. #---------------------------------------------------------------
  310. # reset warning filters & registry before each test
  311. #---------------------------------------------------------------
  312. # flag to reset all warning filters & ignore state
  313. resetWarningState = True
  314. def setUp(self):
  315. super(TestCase, self).setUp()
  316. self.setUpWarnings()
  317. # have uh.debug_only_repr() return real values for duration of test
  318. self.patchAttr(exc, "ENABLE_DEBUG_ONLY_REPR", True)
  319. def setUpWarnings(self):
  320. """helper to init warning filters before subclass setUp()"""
  321. if self.resetWarningState:
  322. ctx = reset_warnings()
  323. ctx.__enter__()
  324. self.addCleanup(ctx.__exit__)
  325. # ignore security warnings, tests may deliberately cause these
  326. # TODO: may want to filter out a few of this, but not blanket filter...
  327. # warnings.filterwarnings("ignore", category=exc.PasslibSecurityWarning)
  328. # ignore warnings about PasswordHash features deprecated in 1.7
  329. # TODO: should be cleaned in 2.0, when support will be dropped.
  330. # should be kept until then, so we test the legacy paths.
  331. warnings.filterwarnings("ignore", r"the method .*\.(encrypt|genconfig|genhash)\(\) is deprecated")
  332. warnings.filterwarnings("ignore", r"the 'vary_rounds' option is deprecated")
  333. warnings.filterwarnings("ignore", r"Support for `(py-bcrypt|bcryptor)` is deprecated")
  334. #---------------------------------------------------------------
  335. # tweak message formatting so longMessage mode is only enabled
  336. # if msg ends with ":", and turn on longMessage by default.
  337. #---------------------------------------------------------------
  338. longMessage = True
  339. def _formatMessage(self, msg, std):
  340. if self.longMessage and msg and msg.rstrip().endswith(":"):
  341. return '%s %s' % (msg.rstrip(), std)
  342. else:
  343. return msg or std
  344. #---------------------------------------------------------------
  345. # override assertRaises() to support '__msg__' keyword,
  346. # and to return the caught exception for further examination
  347. #---------------------------------------------------------------
  348. def assertRaises(self, _exc_type, _callable=None, *args, **kwds):
  349. msg = kwds.pop("__msg__", None)
  350. if _callable is None:
  351. # FIXME: this ignores 'msg'
  352. return super(TestCase, self).assertRaises(_exc_type, None,
  353. *args, **kwds)
  354. try:
  355. result = _callable(*args, **kwds)
  356. except _exc_type as err:
  357. return err
  358. std = "function returned %r, expected it to raise %r" % (result,
  359. _exc_type)
  360. raise self.failureException(self._formatMessage(msg, std))
  361. #---------------------------------------------------------------
  362. # forbid a bunch of deprecated aliases so I stop using them
  363. #---------------------------------------------------------------
  364. def assertEquals(self, *a, **k):
  365. raise AssertionError("this alias is deprecated by unittest2")
  366. assertNotEquals = assertRegexMatches = assertEquals
  367. #===================================================================
  368. # custom methods for matching warnings
  369. #===================================================================
  370. def assertWarning(self, warning,
  371. message_re=None, message=None,
  372. category=None,
  373. filename_re=None, filename=None,
  374. lineno=None,
  375. msg=None,
  376. ):
  377. """check if warning matches specified parameters.
  378. 'warning' is the instance of Warning to match against;
  379. can also be instance of WarningMessage (as returned by catch_warnings).
  380. """
  381. # check input type
  382. if hasattr(warning, "category"):
  383. # resolve WarningMessage -> Warning, but preserve original
  384. wmsg = warning
  385. warning = warning.message
  386. else:
  387. # no original WarningMessage, passed raw Warning
  388. wmsg = None
  389. # tests that can use a warning instance or WarningMessage object
  390. if message:
  391. self.assertEqual(str(warning), message, msg)
  392. if message_re:
  393. self.assertRegex(str(warning), message_re, msg)
  394. if category:
  395. self.assertIsInstance(warning, category, msg)
  396. # tests that require a WarningMessage object
  397. if filename or filename_re:
  398. if not wmsg:
  399. raise TypeError("matching on filename requires a "
  400. "WarningMessage instance")
  401. real = wmsg.filename
  402. if real.endswith(".pyc") or real.endswith(".pyo"):
  403. # FIXME: should use a stdlib call to resolve this back
  404. # to module's original filename.
  405. real = real[:-1]
  406. if filename:
  407. self.assertEqual(real, filename, msg)
  408. if filename_re:
  409. self.assertRegex(real, filename_re, msg)
  410. if lineno:
  411. if not wmsg:
  412. raise TypeError("matching on lineno requires a "
  413. "WarningMessage instance")
  414. self.assertEqual(wmsg.lineno, lineno, msg)
  415. class _AssertWarningList(warnings.catch_warnings):
  416. """context manager for assertWarningList()"""
  417. def __init__(self, case, **kwds):
  418. self.case = case
  419. self.kwds = kwds
  420. self.__super = super(TestCase._AssertWarningList, self)
  421. self.__super.__init__(record=True)
  422. def __enter__(self):
  423. self.log = self.__super.__enter__()
  424. def __exit__(self, *exc_info):
  425. self.__super.__exit__(*exc_info)
  426. if exc_info[0] is None:
  427. self.case.assertWarningList(self.log, **self.kwds)
  428. def assertWarningList(self, wlist=None, desc=None, msg=None):
  429. """check that warning list (e.g. from catch_warnings) matches pattern"""
  430. if desc is None:
  431. assert wlist is not None
  432. return self._AssertWarningList(self, desc=wlist, msg=msg)
  433. # TODO: make this display better diff of *which* warnings did not match
  434. assert desc is not None
  435. if not isinstance(desc, (list,tuple)):
  436. desc = [desc]
  437. for idx, entry in enumerate(desc):
  438. if isinstance(entry, str):
  439. entry = dict(message_re=entry)
  440. elif isinstance(entry, type) and issubclass(entry, Warning):
  441. entry = dict(category=entry)
  442. elif not isinstance(entry, dict):
  443. raise TypeError("entry must be str, warning, or dict")
  444. try:
  445. data = wlist[idx]
  446. except IndexError:
  447. break
  448. self.assertWarning(data, msg=msg, **entry)
  449. else:
  450. if len(wlist) == len(desc):
  451. return
  452. std = "expected %d warnings, found %d: wlist=%s desc=%r" % \
  453. (len(desc), len(wlist), self._formatWarningList(wlist), desc)
  454. raise self.failureException(self._formatMessage(msg, std))
  455. def consumeWarningList(self, wlist, desc=None, *args, **kwds):
  456. """[deprecated] assertWarningList() variant that clears list afterwards"""
  457. if desc is None:
  458. desc = []
  459. self.assertWarningList(wlist, desc, *args, **kwds)
  460. del wlist[:]
  461. def _formatWarning(self, entry):
  462. tail = ""
  463. if hasattr(entry, "message"):
  464. # WarningMessage instance.
  465. tail = " filename=%r lineno=%r" % (entry.filename, entry.lineno)
  466. if entry.line:
  467. tail += " line=%r" % (entry.line,)
  468. entry = entry.message
  469. cls = type(entry)
  470. return "<%s.%s message=%r%s>" % (cls.__module__, cls.__name__,
  471. str(entry), tail)
  472. def _formatWarningList(self, wlist):
  473. return "[%s]" % ", ".join(self._formatWarning(entry) for entry in wlist)
  474. #===================================================================
  475. # capability tests
  476. #===================================================================
  477. def require_stringprep(self):
  478. """helper to skip test if stringprep is missing"""
  479. from passlib.utils import stringprep
  480. if not stringprep:
  481. from passlib.utils import _stringprep_missing_reason
  482. raise self.skipTest("not available - stringprep module is " +
  483. _stringprep_missing_reason)
  484. def require_TEST_MODE(self, level):
  485. """skip test for all PASSLIB_TEST_MODE values below <level>"""
  486. if not TEST_MODE(level):
  487. raise self.skipTest("requires >= %r test mode" % level)
  488. def require_writeable_filesystem(self):
  489. """skip test if writeable FS not available"""
  490. if GAE:
  491. return self.skipTest("GAE doesn't offer read/write filesystem access")
  492. #===================================================================
  493. # reproducible random helpers
  494. #===================================================================
  495. #: global thread lock for random state
  496. #: XXX: could split into global & per-instance locks if need be
  497. _random_global_lock = threading.Lock()
  498. #: cache of global seed value, initialized on first call to getRandom()
  499. _random_global_seed = None
  500. #: per-instance cache of name -> RNG
  501. _random_cache = None
  502. def getRandom(self, name="default", seed=None):
  503. """
  504. Return a :class:`random.Random` object for current test method to use.
  505. Within an instance, multiple calls with the same name will return
  506. the same object.
  507. When first created, each RNG will be seeded with value derived from
  508. a global seed, the test class module & name, the current test method name,
  509. and the **name** parameter.
  510. The global seed taken from the $RANDOM_TEST_SEED env var,
  511. the $PYTHONHASHSEED env var, or a randomly generated the
  512. first time this method is called. In all cases, the value
  513. is logged for reproducibility.
  514. :param name:
  515. name to uniquely identify separate RNGs w/in a test
  516. (e.g. for threaded tests).
  517. :param seed:
  518. override global seed when initialzing rng.
  519. :rtype: random.Random
  520. """
  521. # check cache
  522. cache = self._random_cache
  523. if cache and name in cache:
  524. return cache[name]
  525. with self._random_global_lock:
  526. # check cache again, and initialize it
  527. cache = self._random_cache
  528. if cache and name in cache:
  529. return cache[name]
  530. elif not cache:
  531. cache = self._random_cache = {}
  532. # init global seed
  533. global_seed = seed or TestCase._random_global_seed
  534. if global_seed is None:
  535. # NOTE: checking PYTHONHASHSEED, because if that's set,
  536. # the test runner wants something reproducible.
  537. global_seed = TestCase._random_global_seed = \
  538. int(os.environ.get("RANDOM_TEST_SEED") or
  539. os.environ.get("PYTHONHASHSEED") or
  540. sys_rng.getrandbits(32))
  541. # XXX: would it be better to print() this?
  542. log.info("using RANDOM_TEST_SEED=%d", global_seed)
  543. # create seed
  544. cls = type(self)
  545. source = "\n".join([str(global_seed), cls.__module__, cls.__name__,
  546. self._testMethodName, name])
  547. digest = hashlib.sha256(source.encode("utf-8")).hexdigest()
  548. seed = int(digest[:16], 16)
  549. # create rng
  550. value = cache[name] = random.Random(seed)
  551. return value
  552. #===================================================================
  553. # subtests
  554. #===================================================================
  555. has_real_subtest = hasattr(_TestCase, "subTest")
  556. @contextlib.contextmanager
  557. def subTest(self, *args, **kwds):
  558. """
  559. wrapper/backport for .subTest() which also traps SkipTest errors.
  560. (see source for details)
  561. """
  562. # this function works around two things:
  563. # * TestCase.subTest() wasn't added until Py34; so for older python versions,
  564. # we either need unittest2 installed, or provide stub of our own.
  565. # this method provides a stub if needed (based on .has_real_subtest check)
  566. #
  567. # * as 2020-10-08, .subTest() doesn't play nicely w/ .skipTest();
  568. # and also makes it hard to debug which subtest had a failure.
  569. # (see https://bugs.python.org/issue25894 and https://bugs.python.org/issue35327)
  570. # this method traps skipTest exceptions, and adds some logging to help debug
  571. # which subtest caused the issue.
  572. # setup way to log subtest info
  573. # XXX: would like better way to inject messages into test output;
  574. # but this at least gets us something for debugging...
  575. # NOTE: this hack will miss parent params if called from nested .subTest()
  576. def _render_title(_msg=None, **params):
  577. out = ("[%s] " % _msg if _msg else "")
  578. if params:
  579. out += "(%s)" % " ".join("%s=%r" % tuple(item) for item in params.items())
  580. return out.strip() or "<subtest>"
  581. test_log = self.getLogger()
  582. title = _render_title(*args, **kwds)
  583. # use real subtest manager if available
  584. if self.has_real_subtest:
  585. ctx = super(TestCase, self).subTest(*args, **kwds)
  586. else:
  587. ctx = nullcontext()
  588. # run the subtest
  589. with ctx:
  590. test_log.info("running subtest: %s", title)
  591. try:
  592. yield
  593. except SkipTest:
  594. # silence "SkipTest" exceptions, want to keep running next subtest.
  595. test_log.info("subtest skipped: %s", title)
  596. pass
  597. except Exception as err:
  598. # log unhandled exception occurred
  599. # (assuming traceback will be reported up higher, so not bothering here)
  600. test_log.warning("subtest failed: %s: %s: %r", title, type(err).__name__, str(err))
  601. raise
  602. # XXX: check for "failed" state in ``self._outcome`` before writing this?
  603. test_log.info("subtest passed: %s", title)
  604. #===================================================================
  605. # other
  606. #===================================================================
  607. _mktemp_queue = None
  608. def mktemp(self, *args, **kwds):
  609. """create temp file that's cleaned up at end of test"""
  610. self.require_writeable_filesystem()
  611. fd, path = tempfile.mkstemp(*args, **kwds)
  612. os.close(fd)
  613. queue = self._mktemp_queue
  614. if queue is None:
  615. queue = self._mktemp_queue = []
  616. def cleaner():
  617. for path in queue:
  618. if os.path.exists(path):
  619. os.remove(path)
  620. del queue[:]
  621. self.addCleanup(cleaner)
  622. queue.append(path)
  623. return path
  624. def patchAttr(self, obj, attr, value, require_existing=True, wrap=False):
  625. """monkeypatch object value, restoring original value on cleanup"""
  626. try:
  627. orig = getattr(obj, attr)
  628. except AttributeError:
  629. if require_existing:
  630. raise
  631. def cleanup():
  632. try:
  633. delattr(obj, attr)
  634. except AttributeError:
  635. pass
  636. self.addCleanup(cleanup)
  637. else:
  638. self.addCleanup(setattr, obj, attr, orig)
  639. if wrap:
  640. value = partial(value, orig)
  641. wraps(orig)(value)
  642. setattr(obj, attr, value)
  643. def getLogger(self):
  644. """
  645. return logger named after current test.
  646. """
  647. cls = type(self)
  648. # NOTE: conditional on qualname for PY2 compat
  649. path = cls.__module__ + "." + getattr(cls, "__qualname__", cls.__name__)
  650. name = self._testMethodName
  651. if name:
  652. path = path + "." + name
  653. return logging.getLogger(path)
  654. #===================================================================
  655. # eoc
  656. #===================================================================
  657. #=============================================================================
  658. # other unittest helpers
  659. #=============================================================================
  660. RESERVED_BACKEND_NAMES = ["any", "default"]
  661. def doesnt_require_backend(func):
  662. """
  663. decorator for HandlerCase.create_backend_case() --
  664. used to decorate methods that should be run even if backend isn't present
  665. (by default, full test suite is skipped when backend is missing)
  666. NOTE: tests decorated with this should not rely on handler have expected (or any!) backend.
  667. """
  668. func._doesnt_require_backend = True
  669. return func
  670. class HandlerCase(TestCase):
  671. """base class for testing password hash handlers (esp passlib.utils.handlers subclasses)
  672. In order to use this to test a handler,
  673. create a subclass will all the appropriate attributes
  674. filled as listed in the example below,
  675. and run the subclass via unittest.
  676. .. todo::
  677. Document all of the options HandlerCase offers.
  678. .. note::
  679. This is subclass of :class:`unittest.TestCase`
  680. (or :class:`unittest2.TestCase` if available).
  681. """
  682. #===================================================================
  683. # class attrs - should be filled in by subclass
  684. #===================================================================
  685. #---------------------------------------------------------------
  686. # handler setup
  687. #---------------------------------------------------------------
  688. # handler class to test [required]
  689. handler = None
  690. # if set, run tests against specified backend
  691. backend = None
  692. #---------------------------------------------------------------
  693. # test vectors
  694. #---------------------------------------------------------------
  695. # list of (secret, hash) tuples which are known to be correct
  696. known_correct_hashes = []
  697. # list of (config, secret, hash) tuples are known to be correct
  698. known_correct_configs = []
  699. # list of (alt_hash, secret, hash) tuples, where alt_hash is a hash
  700. # using an alternate representation that should be recognized and verify
  701. # correctly, but should be corrected to match hash when passed through
  702. # genhash()
  703. known_alternate_hashes = []
  704. # hashes so malformed they aren't even identified properly
  705. known_unidentified_hashes = []
  706. # hashes which are identifiabled but malformed - they should identify()
  707. # as True, but cause an error when passed to genhash/verify.
  708. known_malformed_hashes = []
  709. # list of (handler name, hash) pairs for other algorithm's hashes that
  710. # handler shouldn't identify as belonging to it this list should generally
  711. # be sufficient (if handler name in list, that entry will be skipped)
  712. known_other_hashes = [
  713. ('des_crypt', '6f8c114b58f2c'),
  714. ('md5_crypt', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'),
  715. ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywW"
  716. "vt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"),
  717. ]
  718. # passwords used to test basic hash behavior - generally
  719. # don't need to be overidden.
  720. stock_passwords = [
  721. u("test"),
  722. u("\u20AC\u00A5$"),
  723. b'\xe2\x82\xac\xc2\xa5$'
  724. ]
  725. #---------------------------------------------------------------
  726. # option flags
  727. #---------------------------------------------------------------
  728. # whether hash is case insensitive
  729. # True, False, or special value "verify-only" (which indicates
  730. # hash contains case-sensitive portion, but verifies is case-insensitive)
  731. secret_case_insensitive = False
  732. # flag if scheme accepts ALL hash strings (e.g. plaintext)
  733. accepts_all_hashes = False
  734. # flag if scheme has "is_disabled" set, and contains 'salted' data
  735. disabled_contains_salt = False
  736. # flag/hack to filter PasslibHashWarning issued by test_72_configs()
  737. filter_config_warnings = False
  738. # forbid certain characters in passwords
  739. @classproperty
  740. def forbidden_characters(cls):
  741. # anything that supports crypt() interface should forbid null chars,
  742. # since crypt() uses null-terminated strings.
  743. if 'os_crypt' in getattr(cls.handler, "backends", ()):
  744. return b"\x00"
  745. return None
  746. #===================================================================
  747. # internal class attrs
  748. #===================================================================
  749. __unittest_skip = True
  750. @property
  751. def descriptionPrefix(self):
  752. handler = self.handler
  753. name = handler.name
  754. if hasattr(handler, "get_backend"):
  755. name += " (%s backend)" % (handler.get_backend(),)
  756. return name
  757. #===================================================================
  758. # support methods
  759. #===================================================================
  760. #---------------------------------------------------------------
  761. # configuration helpers
  762. #---------------------------------------------------------------
  763. @classmethod
  764. def iter_known_hashes(cls):
  765. """iterate through known (secret, hash) pairs"""
  766. for secret, hash in cls.known_correct_hashes:
  767. yield secret, hash
  768. for config, secret, hash in cls.known_correct_configs:
  769. yield secret, hash
  770. for alt, secret, hash in cls.known_alternate_hashes:
  771. yield secret, hash
  772. def get_sample_hash(self):
  773. """test random sample secret/hash pair"""
  774. known = list(self.iter_known_hashes())
  775. return self.getRandom().choice(known)
  776. #---------------------------------------------------------------
  777. # test helpers
  778. #---------------------------------------------------------------
  779. def check_verify(self, secret, hash, msg=None, negate=False):
  780. """helper to check verify() outcome, honoring is_disabled_handler"""
  781. result = self.do_verify(secret, hash)
  782. self.assertTrue(result is True or result is False,
  783. "verify() returned non-boolean value: %r" % (result,))
  784. if self.handler.is_disabled or negate:
  785. if not result:
  786. return
  787. if not msg:
  788. msg = ("verify incorrectly returned True: secret=%r, hash=%r" %
  789. (secret, hash))
  790. raise self.failureException(msg)
  791. else:
  792. if result:
  793. return
  794. if not msg:
  795. msg = "verify failed: secret=%r, hash=%r" % (secret, hash)
  796. raise self.failureException(msg)
  797. def check_returned_native_str(self, result, func_name):
  798. self.assertIsInstance(result, str,
  799. "%s() failed to return native string: %r" % (func_name, result,))
  800. #---------------------------------------------------------------
  801. # PasswordHash helpers - wraps all calls to PasswordHash api,
  802. # so that subclasses can fill in defaults and account for other specialized behavior
  803. #---------------------------------------------------------------
  804. def populate_settings(self, kwds):
  805. """subclassable method to populate default settings"""
  806. # use lower rounds settings for certain test modes
  807. handler = self.handler
  808. if 'rounds' in handler.setting_kwds and 'rounds' not in kwds:
  809. mn = handler.min_rounds
  810. df = handler.default_rounds
  811. if TEST_MODE(max="quick"):
  812. # use minimum rounds for quick mode
  813. kwds['rounds'] = max(3, mn)
  814. else:
  815. # use default/16 otherwise
  816. factor = 3
  817. if getattr(handler, "rounds_cost", None) == "log2":
  818. df -= factor
  819. else:
  820. df //= (1<<factor)
  821. kwds['rounds'] = max(3, mn, df)
  822. def populate_context(self, secret, kwds):
  823. """subclassable method allowing 'secret' to be encode context kwds"""
  824. return secret
  825. # TODO: rename to do_hash() to match new API
  826. def do_encrypt(self, secret, use_encrypt=False, handler=None, context=None, **settings):
  827. """call handler's hash() method with specified options"""
  828. self.populate_settings(settings)
  829. if context is None:
  830. context = {}
  831. secret = self.populate_context(secret, context)
  832. if use_encrypt:
  833. # use legacy 1.6 api
  834. warnings = []
  835. if settings:
  836. context.update(**settings)
  837. warnings.append("passing settings to.*is deprecated")
  838. with self.assertWarningList(warnings):
  839. return (handler or self.handler).encrypt(secret, **context)
  840. else:
  841. # use 1.7 api
  842. return (handler or self.handler).using(**settings).hash(secret, **context)
  843. def do_verify(self, secret, hash, handler=None, **kwds):
  844. """call handler's verify method"""
  845. secret = self.populate_context(secret, kwds)
  846. return (handler or self.handler).verify(secret, hash, **kwds)
  847. def do_identify(self, hash):
  848. """call handler's identify method"""
  849. return self.handler.identify(hash)
  850. def do_genconfig(self, **kwds):
  851. """call handler's genconfig method with specified options"""
  852. self.populate_settings(kwds)
  853. return self.handler.genconfig(**kwds)
  854. def do_genhash(self, secret, config, **kwds):
  855. """call handler's genhash method with specified options"""
  856. secret = self.populate_context(secret, kwds)
  857. return self.handler.genhash(secret, config, **kwds)
  858. def do_stub_encrypt(self, handler=None, context=None, **settings):
  859. """
  860. return sample hash for handler, w/o caring if digest is valid
  861. (uses some monkeypatching to minimize digest calculation cost)
  862. """
  863. handler = (handler or self.handler).using(**settings)
  864. if context is None:
  865. context = {}
  866. secret = self.populate_context("", context)
  867. with patch_calc_min_rounds(handler):
  868. return handler.hash(secret, **context)
  869. #---------------------------------------------------------------
  870. # automatically generate subclasses for testing specific backends,
  871. # and other backend helpers
  872. #---------------------------------------------------------------
  873. #: default message used by _get_skip_backend_reason()
  874. _BACKEND_NOT_AVAILABLE = "backend not available"
  875. @classmethod
  876. def _get_skip_backend_reason(cls, backend):
  877. """
  878. helper for create_backend_case() --
  879. returns reason to skip backend, or None if backend should be tested
  880. """
  881. handler = cls.handler
  882. if not is_default_backend(handler, backend) and not TEST_MODE("full"):
  883. return "only default backend is being tested"
  884. if handler.has_backend(backend):
  885. return None
  886. return cls._BACKEND_NOT_AVAILABLE
  887. @classmethod
  888. def create_backend_case(cls, backend):
  889. handler = cls.handler
  890. name = handler.name
  891. assert hasattr(handler, "backends"), "handler must support uh.HasManyBackends protocol"
  892. assert backend in handler.backends, "unknown backend: %r" % (backend,)
  893. bases = (cls,)
  894. if backend == "os_crypt":
  895. bases += (OsCryptMixin,)
  896. subcls = type(
  897. "%s_%s_test" % (name, backend),
  898. bases,
  899. dict(
  900. descriptionPrefix="%s (%s backend)" % (name, backend),
  901. backend=backend,
  902. _skip_backend_reason=cls._get_skip_backend_reason(backend),
  903. __module__=cls.__module__,
  904. )
  905. )
  906. return subcls
  907. #: flag for setUp() indicating this class is disabled due to backend issue;
  908. #: this is only set for dynamic subclasses generated by create_backend_case()
  909. _skip_backend_reason = None
  910. def _test_requires_backend(self):
  911. """
  912. check if current test method decorated with doesnt_require_backend() helper
  913. """
  914. meth = getattr(self, self._testMethodName, None)
  915. return not getattr(meth, "_doesnt_require_backend", False)
  916. #===================================================================
  917. # setup
  918. #===================================================================
  919. def setUp(self):
  920. # check if test is disabled due to missing backend;
  921. # and that it wasn't exempted via @doesnt_require_backend() decorator
  922. test_requires_backend = self._test_requires_backend()
  923. if test_requires_backend and self._skip_backend_reason:
  924. raise self.skipTest(self._skip_backend_reason)
  925. super(HandlerCase, self).setUp()
  926. # if needed, select specific backend for duration of test
  927. # NOTE: skipping this if create_backend_case() signalled we're skipping backend
  928. # (can only get here for @doesnt_require_backend decorated methods)
  929. handler = self.handler
  930. backend = self.backend
  931. if backend:
  932. if not hasattr(handler, "set_backend"):
  933. raise RuntimeError("handler doesn't support multiple backends")
  934. try:
  935. self.addCleanup(handler.set_backend, handler.get_backend())
  936. handler.set_backend(backend)
  937. except uh.exc.MissingBackendError:
  938. if test_requires_backend:
  939. raise
  940. # else test is decorated with @doesnt_require_backend, let it through.
  941. # patch some RNG references so they're reproducible.
  942. from passlib.utils import handlers
  943. self.patchAttr(handlers, "rng", self.getRandom("salt generator"))
  944. #===================================================================
  945. # basic tests
  946. #===================================================================
  947. def test_01_required_attributes(self):
  948. """validate required attributes"""
  949. handler = self.handler
  950. def ga(name):
  951. return getattr(handler, name, None)
  952. #
  953. # name should be a str, and valid
  954. #
  955. name = ga("name")
  956. self.assertTrue(name, "name not defined:")
  957. self.assertIsInstance(name, str, "name must be native str")
  958. self.assertTrue(name.lower() == name, "name not lower-case:")
  959. self.assertTrue(re.match("^[a-z0-9_]+$", name),
  960. "name must be alphanum + underscore: %r" % (name,))
  961. #
  962. # setting_kwds should be specified
  963. #
  964. settings = ga("setting_kwds")
  965. self.assertTrue(settings is not None, "setting_kwds must be defined:")
  966. self.assertIsInstance(settings, tuple, "setting_kwds must be a tuple:")
  967. #
  968. # context_kwds should be specified
  969. #
  970. context = ga("context_kwds")
  971. self.assertTrue(context is not None, "context_kwds must be defined:")
  972. self.assertIsInstance(context, tuple, "context_kwds must be a tuple:")
  973. # XXX: any more checks needed?
  974. def test_02_config_workflow(self):
  975. """test basic config-string workflow
  976. this tests that genconfig() returns the expected types,
  977. and that identify() and genhash() handle the result correctly.
  978. """
  979. #
  980. # genconfig() should return native string.
  981. # NOTE: prior to 1.7 could return None, but that's no longer allowed.
  982. #
  983. config = self.do_genconfig()
  984. self.check_returned_native_str(config, "genconfig")
  985. #
  986. # genhash() should always accept genconfig()'s output,
  987. # whether str OR None.
  988. #
  989. result = self.do_genhash('stub', config)
  990. self.check_returned_native_str(result, "genhash")
  991. #
  992. # verify() should never accept config strings
  993. #
  994. # NOTE: changed as of 1.7 -- previously, .verify() should have
  995. # rejected partial config strings returned by genconfig().
  996. # as of 1.7, that feature is deprecated, and genconfig()
  997. # always returns a hash (usually of the empty string)
  998. # so verify should always accept it's output
  999. self.do_verify('', config) # usually true, but not required by protocol
  1000. #
  1001. # identify() should positively identify config strings if not None.
  1002. #
  1003. # NOTE: changed as of 1.7 -- genconfig() previously might return None,
  1004. # now must always return valid hash
  1005. self.assertTrue(self.do_identify(config),
  1006. "identify() failed to identify genconfig() output: %r" %
  1007. (config,))
  1008. def test_02_using_workflow(self):
  1009. """test basic using() workflow"""
  1010. handler = self.handler
  1011. subcls = handler.using()
  1012. self.assertIsNot(subcls, handler)
  1013. self.assertEqual(subcls.name, handler.name)
  1014. # NOTE: other info attrs should match as well, just testing basic behavior.
  1015. # NOTE: mixin-specific args like using(min_rounds=xxx) tested later.
  1016. def test_03_hash_workflow(self, use_16_legacy=False):
  1017. """test basic hash-string workflow.
  1018. this tests that hash()'s hashes are accepted
  1019. by verify() and identify(), and regenerated correctly by genhash().
  1020. the test is run against a couple of different stock passwords.
  1021. """
  1022. wrong_secret = 'stub'
  1023. for secret in self.stock_passwords:
  1024. #
  1025. # hash() should generate native str hash
  1026. #
  1027. result = self.do_encrypt(secret, use_encrypt=use_16_legacy)
  1028. self.check_returned_native_str(result, "hash")
  1029. #
  1030. # verify() should work only against secret
  1031. #
  1032. self.check_verify(secret, result)
  1033. self.check_verify(wrong_secret, result, negate=True)
  1034. #
  1035. # genhash() should reproduce original hash
  1036. #
  1037. other = self.do_genhash(secret, result)
  1038. self.check_returned_native_str(other, "genhash")
  1039. if self.handler.is_disabled and self.disabled_contains_salt:
  1040. self.assertNotEqual(other, result, "genhash() failed to salt result "
  1041. "hash: secret=%r hash=%r: result=%r" %
  1042. (secret, result, other))
  1043. else:
  1044. self.assertEqual(other, result, "genhash() failed to reproduce "
  1045. "hash: secret=%r hash=%r: result=%r" %
  1046. (secret, result, other))
  1047. #
  1048. # genhash() should NOT reproduce original hash for wrong password
  1049. #
  1050. other = self.do_genhash(wrong_secret, result)
  1051. self.check_returned_native_str(other, "genhash")
  1052. if self.handler.is_disabled and not self.disabled_contains_salt:
  1053. self.assertEqual(other, result, "genhash() failed to reproduce "
  1054. "disabled-hash: secret=%r hash=%r other_secret=%r: result=%r" %
  1055. (secret, result, wrong_secret, other))
  1056. else:
  1057. self.assertNotEqual(other, result, "genhash() duplicated "
  1058. "hash: secret=%r hash=%r wrong_secret=%r: result=%r" %
  1059. (secret, result, wrong_secret, other))
  1060. #
  1061. # identify() should positively identify hash
  1062. #
  1063. self.assertTrue(self.do_identify(result))
  1064. def test_03_legacy_hash_workflow(self):
  1065. """test hash-string workflow with legacy .encrypt() & .genhash() methods"""
  1066. self.test_03_hash_workflow(use_16_legacy=True)
  1067. def test_04_hash_types(self):
  1068. """test hashes can be unicode or bytes"""
  1069. # this runs through workflow similar to 03, but wraps
  1070. # everything using tonn() so we test unicode under py2,
  1071. # and bytes under py3.
  1072. # hash using non-native secret
  1073. result = self.do_encrypt(tonn('stub'))
  1074. self.check_returned_native_str(result, "hash")
  1075. # verify using non-native hash
  1076. self.check_verify('stub', tonn(result))
  1077. # verify using non-native hash AND secret
  1078. self.check_verify(tonn('stub'), tonn(result))
  1079. # genhash using non-native hash
  1080. other = self.do_genhash('stub', tonn(result))
  1081. self.check_returned_native_str(other, "genhash")
  1082. if self.handler.is_disabled and self.disabled_contains_salt:
  1083. self.assertNotEqual(other, result)
  1084. else:
  1085. self.assertEqual(other, result)
  1086. # genhash using non-native hash AND secret
  1087. other = self.do_genhash(tonn('stub'), tonn(result))
  1088. self.check_returned_native_str(other, "genhash")
  1089. if self.handler.is_disabled and self.disabled_contains_salt:
  1090. self.assertNotEqual(other, result)
  1091. else:
  1092. self.assertEqual(other, result)
  1093. # identify using non-native hash
  1094. self.assertTrue(self.do_identify(tonn(result)))
  1095. def test_05_backends(self):
  1096. """test multi-backend support"""
  1097. # check that handler supports multiple backends
  1098. handler = self.handler
  1099. if not hasattr(handler, "set_backend"):
  1100. raise self.skipTest("handler only has one backend")
  1101. # add cleanup func to restore old backend
  1102. self.addCleanup(handler.set_backend, handler.get_backend())
  1103. # run through each backend, make sure it works
  1104. for backend in handler.backends:
  1105. #
  1106. # validate backend name
  1107. #
  1108. self.assertIsInstance(backend, str)
  1109. self.assertNotIn(backend, RESERVED_BACKEND_NAMES,
  1110. "invalid backend name: %r" % (backend,))
  1111. #
  1112. # ensure has_backend() returns bool value
  1113. #
  1114. ret = handler.has_backend(backend)
  1115. if ret is True:
  1116. # verify backend can be loaded
  1117. handler.set_backend(backend)
  1118. self.assertEqual(handler.get_backend(), backend)
  1119. elif ret is False:
  1120. # verify backend CAN'T be loaded
  1121. self.assertRaises(MissingBackendError, handler.set_backend,
  1122. backend)
  1123. else:
  1124. # didn't return boolean object. commonly fails due to
  1125. # use of 'classmethod' decorator instead of 'classproperty'
  1126. raise TypeError("has_backend(%r) returned invalid "
  1127. "value: %r" % (backend, ret))
  1128. #===================================================================
  1129. # salts
  1130. #===================================================================
  1131. def require_salt(self):
  1132. if 'salt' not in self.handler.setting_kwds:
  1133. raise self.skipTest("handler doesn't have salt")
  1134. def require_salt_info(self):
  1135. self.require_salt()
  1136. if not has_salt_info(self.handler):
  1137. raise self.skipTest("handler doesn't provide salt info")
  1138. def test_10_optional_salt_attributes(self):
  1139. """validate optional salt attributes"""
  1140. self.require_salt_info()
  1141. AssertionError = self.failureException
  1142. cls = self.handler
  1143. # check max_salt_size
  1144. mx_set = (cls.max_salt_size is not None)
  1145. if mx_set and cls.max_salt_size < 1:
  1146. raise AssertionError("max_salt_chars must be >= 1")
  1147. # check min_salt_size
  1148. if cls.min_salt_size < 0:
  1149. raise AssertionError("min_salt_chars must be >= 0")
  1150. if mx_set and cls.min_salt_size > cls.max_salt_size:
  1151. raise AssertionError("min_salt_chars must be <= max_salt_chars")
  1152. # check default_salt_size
  1153. if cls.default_salt_size < cls.min_salt_size:
  1154. raise AssertionError("default_salt_size must be >= min_salt_size")
  1155. if mx_set and cls.default_salt_size > cls.max_salt_size:
  1156. raise AssertionError("default_salt_size must be <= max_salt_size")
  1157. # check for 'salt_size' keyword
  1158. # NOTE: skipping warning if default salt size is already maxed out
  1159. # (might change that in future)
  1160. if 'salt_size' not in cls.setting_kwds and (not mx_set or cls.default_salt_size < cls.max_salt_size):
  1161. warn('%s: hash handler supports range of salt sizes, '
  1162. 'but doesn\'t offer \'salt_size\' setting' % (cls.name,))
  1163. # check salt_chars & default_salt_chars
  1164. if cls.salt_chars:
  1165. if not cls.default_salt_chars:
  1166. raise AssertionError("default_salt_chars must not be empty")
  1167. for c in cls.default_salt_chars:
  1168. if c not in cls.salt_chars:
  1169. raise AssertionError("default_salt_chars must be subset of salt_chars: %r not in salt_chars" % (c,))
  1170. else:
  1171. if not cls.default_salt_chars:
  1172. raise AssertionError("default_salt_chars MUST be specified if salt_chars is empty")
  1173. @property
  1174. def salt_bits(self):
  1175. """calculate number of salt bits in hash"""
  1176. # XXX: replace this with bitsize() method?
  1177. handler = self.handler
  1178. assert has_salt_info(handler), "need explicit bit-size for " + handler.name
  1179. from math import log
  1180. # FIXME: this may be off for case-insensitive hashes, but that accounts
  1181. # for ~1 bit difference, which is good enough for test_11()
  1182. return int(handler.default_salt_size *
  1183. log(len(handler.default_salt_chars), 2))
  1184. def test_11_unique_salt(self):
  1185. """test hash() / genconfig() creates new salt each time"""
  1186. self.require_salt()
  1187. # odds of picking 'n' identical salts at random is '(.5**salt_bits)**n'.
  1188. # we want to pick the smallest N needed s.t. odds are <1/10**d, just
  1189. # to eliminate false-positives. which works out to n>3.33+d-salt_bits.
  1190. # for 1/1e12 odds, n=1 is sufficient for most hashes, but a few border cases (e.g.
  1191. # cisco_type7) have < 16 bits of salt, requiring more.
  1192. samples = max(1, 4 + 12 - self.salt_bits)
  1193. def sampler(func):
  1194. value1 = func()
  1195. for _ in irange(samples):
  1196. value2 = func()
  1197. if value1 != value2:
  1198. return
  1199. raise self.failureException("failed to find different salt after "
  1200. "%d samples" % (samples,))
  1201. sampler(self.do_genconfig)
  1202. sampler(lambda: self.do_encrypt("stub"))
  1203. def test_12_min_salt_size(self):
  1204. """test hash() / genconfig() honors min_salt_size"""
  1205. self.require_salt_info()
  1206. handler = self.handler
  1207. salt_char = handler.salt_chars[0:1]
  1208. min_size = handler.min_salt_size
  1209. #
  1210. # check min is accepted
  1211. #
  1212. s1 = salt_char * min_size
  1213. self.do_genconfig(salt=s1)
  1214. self.do_encrypt('stub', salt_size=min_size)
  1215. #
  1216. # check min-1 is rejected
  1217. #
  1218. if min_size > 0:
  1219. self.assertRaises(ValueError, self.do_genconfig,
  1220. salt=s1[:-1])
  1221. self.assertRaises(ValueError, self.do_encrypt, 'stub',
  1222. salt_size=min_size-1)
  1223. def test_13_max_salt_size(self):
  1224. """test hash() / genconfig() honors max_salt_size"""
  1225. self.require_salt_info()
  1226. handler = self.handler
  1227. max_size = handler.max_salt_size
  1228. salt_char = handler.salt_chars[0:1]
  1229. # NOTE: skipping this for hashes like argon2 since max_salt_size takes WAY too much memory
  1230. if max_size is None or max_size > (1 << 20):
  1231. #
  1232. # if it's not set, salt should never be truncated; so test it
  1233. # with an unreasonably large salt.
  1234. #
  1235. s1 = salt_char * 1024
  1236. c1 = self.do_stub_encrypt(salt=s1)
  1237. c2 = self.do_stub_encrypt(salt=s1 + salt_char)
  1238. self.assertNotEqual(c1, c2)
  1239. self.do_stub_encrypt(salt_size=1024)
  1240. else:
  1241. #
  1242. # check max size is accepted
  1243. #
  1244. s1 = salt_char * max_size
  1245. c1 = self.do_stub_encrypt(salt=s1)
  1246. self.do_stub_encrypt(salt_size=max_size)
  1247. #
  1248. # check max size + 1 is rejected
  1249. #
  1250. s2 = s1 + salt_char
  1251. self.assertRaises(ValueError, self.do_stub_encrypt, salt=s2)
  1252. self.assertRaises(ValueError, self.do_stub_encrypt, salt_size=max_size + 1)
  1253. #
  1254. # should accept too-large salt in relaxed mode
  1255. #
  1256. if has_relaxed_setting(handler):
  1257. with warnings.catch_warnings(record=True): # issues passlibhandlerwarning
  1258. c2 = self.do_stub_encrypt(salt=s2, relaxed=True)
  1259. self.assertEqual(c2, c1)
  1260. #
  1261. # if min_salt supports it, check smaller than mx is NOT truncated
  1262. #
  1263. if handler.min_salt_size < max_size:
  1264. c3 = self.do_stub_encrypt(salt=s1[:-1])
  1265. self.assertNotEqual(c3, c1)
  1266. # whether salt should be passed through bcrypt repair function
  1267. fuzz_salts_need_bcrypt_repair = False
  1268. def prepare_salt(self, salt):
  1269. """prepare generated salt"""
  1270. if self.fuzz_salts_need_bcrypt_repair:
  1271. from passlib.utils.binary import bcrypt64
  1272. salt = bcrypt64.repair_unused(salt)
  1273. return salt
  1274. def test_14_salt_chars(self):
  1275. """test hash() honors salt_chars"""
  1276. self.require_salt_info()
  1277. handler = self.handler
  1278. mx = handler.max_salt_size
  1279. mn = handler.min_salt_size
  1280. cs = handler.salt_chars
  1281. raw = isinstance(cs, bytes)
  1282. # make sure all listed chars are accepted
  1283. for salt in batch(cs, mx or 32):
  1284. if len(salt) < mn:
  1285. salt = repeat_string(salt, mn)
  1286. salt = self.prepare_salt(salt)
  1287. self.do_stub_encrypt(salt=salt)
  1288. # check some invalid salt chars, make sure they're rejected
  1289. source = u('\x00\xff')
  1290. if raw:
  1291. source = source.encode("latin-1")
  1292. chunk = max(mn, 1)
  1293. for c in source:
  1294. if c not in cs:
  1295. self.assertRaises(ValueError, self.do_stub_encrypt, salt=c*chunk,
  1296. __msg__="invalid salt char %r:" % (c,))
  1297. @property
  1298. def salt_type(self):
  1299. """hack to determine salt keyword's datatype"""
  1300. # NOTE: cisco_type7 uses 'int'
  1301. if getattr(self.handler, "_salt_is_bytes", False):
  1302. return bytes
  1303. else:
  1304. return unicode
  1305. def test_15_salt_type(self):
  1306. """test non-string salt values"""
  1307. self.require_salt()
  1308. salt_type = self.salt_type
  1309. salt_size = getattr(self.handler, "min_salt_size", 0) or 8
  1310. # should always throw error for random class.
  1311. class fake(object):
  1312. pass
  1313. self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=fake())
  1314. # unicode should be accepted only if salt_type is unicode.
  1315. if salt_type is not unicode:
  1316. self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=u('x') * salt_size)
  1317. # bytes should be accepted only if salt_type is bytes,
  1318. # OR if salt type is unicode and running PY2 - to allow native strings.
  1319. if not (salt_type is bytes or (PY2 and salt_type is unicode)):
  1320. self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=b'x' * salt_size)
  1321. def test_using_salt_size(self):
  1322. """Handler.using() -- default_salt_size"""
  1323. self.require_salt_info()
  1324. handler = self.handler
  1325. mn = handler.min_salt_size
  1326. mx = handler.max_salt_size
  1327. df = handler.default_salt_size
  1328. # should prevent setting below handler limit
  1329. self.assertRaises(ValueError, handler.using, default_salt_size=-1)
  1330. with self.assertWarningList([PasslibHashWarning]):
  1331. temp = handler.using(default_salt_size=-1, relaxed=True)
  1332. self.assertEqual(temp.default_salt_size, mn)
  1333. # should prevent setting above handler limit
  1334. if mx:
  1335. self.assertRaises(ValueError, handler.using, default_salt_size=mx+1)
  1336. with self.assertWarningList([PasslibHashWarning]):
  1337. temp = handler.using(default_salt_size=mx+1, relaxed=True)
  1338. self.assertEqual(temp.default_salt_size, mx)
  1339. # try setting to explicit value
  1340. if mn != mx:
  1341. temp = handler.using(default_salt_size=mn+1)
  1342. self.assertEqual(temp.default_salt_size, mn+1)
  1343. self.assertEqual(handler.default_salt_size, df)
  1344. temp = handler.using(default_salt_size=mn+2)
  1345. self.assertEqual(temp.default_salt_size, mn+2)
  1346. self.assertEqual(handler.default_salt_size, df)
  1347. # accept strings
  1348. if mn == mx:
  1349. ref = mn
  1350. else:
  1351. ref = mn + 1
  1352. temp = handler.using(default_salt_size=str(ref))
  1353. self.assertEqual(temp.default_salt_size, ref)
  1354. # reject invalid strings
  1355. self.assertRaises(ValueError, handler.using, default_salt_size=str(ref) + "xxx")
  1356. # honor 'salt_size' alias
  1357. temp = handler.using(salt_size=ref)
  1358. self.assertEqual(temp.default_salt_size, ref)
  1359. #===================================================================
  1360. # rounds
  1361. #===================================================================
  1362. def require_rounds_info(self):
  1363. if not has_rounds_info(self.handler):
  1364. raise self.skipTest("handler lacks rounds attributes")
  1365. def test_20_optional_rounds_attributes(self):
  1366. """validate optional rounds attributes"""
  1367. self.require_rounds_info()
  1368. cls = self.handler
  1369. AssertionError = self.failureException
  1370. # check max_rounds
  1371. if cls.max_rounds is None:
  1372. raise AssertionError("max_rounds not specified")
  1373. if cls.max_rounds < 1:
  1374. raise AssertionError("max_rounds must be >= 1")
  1375. # check min_rounds
  1376. if cls.min_rounds < 0:
  1377. raise AssertionError("min_rounds must be >= 0")
  1378. if cls.min_rounds > cls.max_rounds:
  1379. raise AssertionError("min_rounds must be <= max_rounds")
  1380. # check default_rounds
  1381. if cls.default_rounds is not None:
  1382. if cls.default_rounds < cls.min_rounds:
  1383. raise AssertionError("default_rounds must be >= min_rounds")
  1384. if cls.default_rounds > cls.max_rounds:
  1385. raise AssertionError("default_rounds must be <= max_rounds")
  1386. # check rounds_cost
  1387. if cls.rounds_cost not in rounds_cost_values:
  1388. raise AssertionError("unknown rounds cost constant: %r" % (cls.rounds_cost,))
  1389. def test_21_min_rounds(self):
  1390. """test hash() / genconfig() honors min_rounds"""
  1391. self.require_rounds_info()
  1392. handler = self.handler
  1393. min_rounds = handler.min_rounds
  1394. # check min is accepted
  1395. self.do_genconfig(rounds=min_rounds)
  1396. self.do_encrypt('stub', rounds=min_rounds)
  1397. # check min-1 is rejected
  1398. self.assertRaises(ValueError, self.do_genconfig, rounds=min_rounds-1)
  1399. self.assertRaises(ValueError, self.do_encrypt, 'stub', rounds=min_rounds-1)
  1400. # TODO: check relaxed mode clips min-1
  1401. def test_21b_max_rounds(self):
  1402. """test hash() / genconfig() honors max_rounds"""
  1403. self.require_rounds_info()
  1404. handler = self.handler
  1405. max_rounds = handler.max_rounds
  1406. if max_rounds is not None:
  1407. # check max+1 is rejected
  1408. self.assertRaises(ValueError, self.do_genconfig, rounds=max_rounds+1)
  1409. self.assertRaises(ValueError, self.do_encrypt, 'stub', rounds=max_rounds+1)
  1410. # handle max rounds
  1411. if max_rounds is None:
  1412. self.do_stub_encrypt(rounds=(1 << 31) - 1)
  1413. else:
  1414. self.do_stub_encrypt(rounds=max_rounds)
  1415. # TODO: check relaxed mode clips max+1
  1416. #--------------------------------------------------------------------------------------
  1417. # HasRounds.using() / .needs_update() -- desired rounds limits
  1418. #--------------------------------------------------------------------------------------
  1419. def _create_using_rounds_helper(self):
  1420. """
  1421. setup test helpers for testing handler.using()'s rounds parameters.
  1422. """
  1423. self.require_rounds_info()
  1424. handler = self.handler
  1425. if handler.name == "bsdi_crypt":
  1426. # hack to bypass bsdi-crypt's "odd rounds only" behavior, messes up this test
  1427. orig_handler = handler
  1428. handler = handler.using()
  1429. handler._generate_rounds = classmethod(lambda cls: super(orig_handler, cls)._generate_rounds())
  1430. # create some fake values to test with
  1431. orig_min_rounds = handler.min_rounds
  1432. orig_max_rounds = handler.max_rounds
  1433. orig_default_rounds = handler.default_rounds
  1434. medium = ((orig_max_rounds or 9999) + orig_min_rounds) // 2
  1435. if medium == orig_default_rounds:
  1436. medium += 1
  1437. small = (orig_min_rounds + medium) // 2
  1438. large = ((orig_max_rounds or 9999) + medium) // 2
  1439. if handler.name == "bsdi_crypt":
  1440. # hack to avoid even numbered rounds
  1441. small |= 1
  1442. medium |= 1
  1443. large |= 1
  1444. adj = 2
  1445. else:
  1446. adj = 1
  1447. # create a subclass with small/medium/large as new default desired values
  1448. with self.assertWarningList([]):
  1449. subcls = handler.using(
  1450. min_desired_rounds=small,
  1451. max_desired_rounds=large,
  1452. default_rounds=medium,
  1453. )
  1454. # return helpers
  1455. return handler, subcls, small, medium, large, adj
  1456. def test_has_rounds_using_harness(self):
  1457. """
  1458. HasRounds.using() -- sanity check test harness
  1459. """
  1460. # setup helpers
  1461. self.require_rounds_info()
  1462. handler = self.handler
  1463. orig_min_rounds = handler.min_rounds
  1464. orig_max_rounds = handler.max_rounds
  1465. orig_default_rounds = handler.default_rounds
  1466. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1467. # shouldn't affect original handler at all
  1468. self.assertEqual(handler.min_rounds, orig_min_rounds)
  1469. self.assertEqual(handler.max_rounds, orig_max_rounds)
  1470. self.assertEqual(handler.min_desired_rounds, None)
  1471. self.assertEqual(handler.max_desired_rounds, None)
  1472. self.assertEqual(handler.default_rounds, orig_default_rounds)
  1473. # should affect subcls' desired value, but not hard min/max
  1474. self.assertEqual(subcls.min_rounds, orig_min_rounds)
  1475. self.assertEqual(subcls.max_rounds, orig_max_rounds)
  1476. self.assertEqual(subcls.default_rounds, medium)
  1477. self.assertEqual(subcls.min_desired_rounds, small)
  1478. self.assertEqual(subcls.max_desired_rounds, large)
  1479. def test_has_rounds_using_w_min_rounds(self):
  1480. """
  1481. HasRounds.using() -- min_rounds / min_desired_rounds
  1482. """
  1483. # setup helpers
  1484. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1485. orig_min_rounds = handler.min_rounds
  1486. orig_max_rounds = handler.max_rounds
  1487. orig_default_rounds = handler.default_rounds
  1488. # .using() should clip values below valid minimum, w/ warning
  1489. if orig_min_rounds > 0:
  1490. self.assertRaises(ValueError, handler.using, min_desired_rounds=orig_min_rounds - adj)
  1491. with self.assertWarningList([PasslibHashWarning]):
  1492. temp = handler.using(min_desired_rounds=orig_min_rounds - adj, relaxed=True)
  1493. self.assertEqual(temp.min_desired_rounds, orig_min_rounds)
  1494. # .using() should clip values above valid maximum, w/ warning
  1495. if orig_max_rounds:
  1496. self.assertRaises(ValueError, handler.using, min_desired_rounds=orig_max_rounds + adj)
  1497. with self.assertWarningList([PasslibHashWarning]):
  1498. temp = handler.using(min_desired_rounds=orig_max_rounds + adj, relaxed=True)
  1499. self.assertEqual(temp.min_desired_rounds, orig_max_rounds)
  1500. # .using() should allow values below previous desired minimum, w/o warning
  1501. with self.assertWarningList([]):
  1502. temp = subcls.using(min_desired_rounds=small - adj)
  1503. self.assertEqual(temp.min_desired_rounds, small - adj)
  1504. # .using() should allow values w/in previous range
  1505. temp = subcls.using(min_desired_rounds=small + 2 * adj)
  1506. self.assertEqual(temp.min_desired_rounds, small + 2 * adj)
  1507. # .using() should allow values above previous desired maximum, w/o warning
  1508. with self.assertWarningList([]):
  1509. temp = subcls.using(min_desired_rounds=large + adj)
  1510. self.assertEqual(temp.min_desired_rounds, large + adj)
  1511. # hash() etc should allow explicit values below desired minimum
  1512. # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using()
  1513. self.assertEqual(get_effective_rounds(subcls, small + adj), small + adj)
  1514. self.assertEqual(get_effective_rounds(subcls, small), small)
  1515. with self.assertWarningList([]):
  1516. self.assertEqual(get_effective_rounds(subcls, small - adj), small - adj)
  1517. # 'min_rounds' should be treated as alias for 'min_desired_rounds'
  1518. temp = handler.using(min_rounds=small)
  1519. self.assertEqual(temp.min_desired_rounds, small)
  1520. # should be able to specify strings
  1521. temp = handler.using(min_rounds=str(small))
  1522. self.assertEqual(temp.min_desired_rounds, small)
  1523. # invalid strings should cause error
  1524. self.assertRaises(ValueError, handler.using, min_rounds=str(small) + "xxx")
  1525. def test_has_rounds_replace_w_max_rounds(self):
  1526. """
  1527. HasRounds.using() -- max_rounds / max_desired_rounds
  1528. """
  1529. # setup helpers
  1530. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1531. orig_min_rounds = handler.min_rounds
  1532. orig_max_rounds = handler.max_rounds
  1533. # .using() should clip values below valid minimum w/ warning
  1534. if orig_min_rounds > 0:
  1535. self.assertRaises(ValueError, handler.using, max_desired_rounds=orig_min_rounds - adj)
  1536. with self.assertWarningList([PasslibHashWarning]):
  1537. temp = handler.using(max_desired_rounds=orig_min_rounds - adj, relaxed=True)
  1538. self.assertEqual(temp.max_desired_rounds, orig_min_rounds)
  1539. # .using() should clip values above valid maximum, w/ warning
  1540. if orig_max_rounds:
  1541. self.assertRaises(ValueError, handler.using, max_desired_rounds=orig_max_rounds + adj)
  1542. with self.assertWarningList([PasslibHashWarning]):
  1543. temp = handler.using(max_desired_rounds=orig_max_rounds + adj, relaxed=True)
  1544. self.assertEqual(temp.max_desired_rounds, orig_max_rounds)
  1545. # .using() should clip values below previous minimum, w/ warning
  1546. with self.assertWarningList([PasslibConfigWarning]):
  1547. temp = subcls.using(max_desired_rounds=small - adj)
  1548. self.assertEqual(temp.max_desired_rounds, small)
  1549. # .using() should reject explicit min > max
  1550. self.assertRaises(ValueError, subcls.using,
  1551. min_desired_rounds=medium+adj,
  1552. max_desired_rounds=medium-adj)
  1553. # .using() should allow values w/in previous range
  1554. temp = subcls.using(min_desired_rounds=large - 2 * adj)
  1555. self.assertEqual(temp.min_desired_rounds, large - 2 * adj)
  1556. # .using() should allow values above previous desired maximum, w/o warning
  1557. with self.assertWarningList([]):
  1558. temp = subcls.using(max_desired_rounds=large + adj)
  1559. self.assertEqual(temp.max_desired_rounds, large + adj)
  1560. # hash() etc should allow explicit values above desired minimum, w/o warning
  1561. # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using()
  1562. self.assertEqual(get_effective_rounds(subcls, large - adj), large - adj)
  1563. self.assertEqual(get_effective_rounds(subcls, large), large)
  1564. with self.assertWarningList([]):
  1565. self.assertEqual(get_effective_rounds(subcls, large + adj), large + adj)
  1566. # 'max_rounds' should be treated as alias for 'max_desired_rounds'
  1567. temp = handler.using(max_rounds=large)
  1568. self.assertEqual(temp.max_desired_rounds, large)
  1569. # should be able to specify strings
  1570. temp = handler.using(max_desired_rounds=str(large))
  1571. self.assertEqual(temp.max_desired_rounds, large)
  1572. # invalid strings should cause error
  1573. self.assertRaises(ValueError, handler.using, max_desired_rounds=str(large) + "xxx")
  1574. def test_has_rounds_using_w_default_rounds(self):
  1575. """
  1576. HasRounds.using() -- default_rounds
  1577. """
  1578. # setup helpers
  1579. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1580. orig_max_rounds = handler.max_rounds
  1581. # XXX: are there any other cases that need testing?
  1582. # implicit default rounds -- increase to min_rounds
  1583. temp = subcls.using(min_rounds=medium+adj)
  1584. self.assertEqual(temp.default_rounds, medium+adj)
  1585. # implicit default rounds -- decrease to max_rounds
  1586. temp = subcls.using(max_rounds=medium-adj)
  1587. self.assertEqual(temp.default_rounds, medium-adj)
  1588. # explicit default rounds below desired minimum
  1589. # XXX: make this a warning if min is implicit?
  1590. self.assertRaises(ValueError, subcls.using, default_rounds=small-adj)
  1591. # explicit default rounds above desired maximum
  1592. # XXX: make this a warning if max is implicit?
  1593. if orig_max_rounds:
  1594. self.assertRaises(ValueError, subcls.using, default_rounds=large+adj)
  1595. # hash() etc should implicit default rounds, but get overridden
  1596. self.assertEqual(get_effective_rounds(subcls), medium)
  1597. self.assertEqual(get_effective_rounds(subcls, medium+adj), medium+adj)
  1598. # should be able to specify strings
  1599. temp = handler.using(default_rounds=str(medium))
  1600. self.assertEqual(temp.default_rounds, medium)
  1601. # invalid strings should cause error
  1602. self.assertRaises(ValueError, handler.using, default_rounds=str(medium) + "xxx")
  1603. def test_has_rounds_using_w_rounds(self):
  1604. """
  1605. HasRounds.using() -- rounds
  1606. """
  1607. # setup helpers
  1608. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1609. orig_max_rounds = handler.max_rounds
  1610. # 'rounds' should be treated as fallback for min, max, and default
  1611. temp = subcls.using(rounds=medium+adj)
  1612. self.assertEqual(temp.min_desired_rounds, medium+adj)
  1613. self.assertEqual(temp.default_rounds, medium+adj)
  1614. self.assertEqual(temp.max_desired_rounds, medium+adj)
  1615. # 'rounds' should be treated as fallback for min, max, and default
  1616. temp = subcls.using(rounds=medium+1, min_rounds=small+adj,
  1617. default_rounds=medium, max_rounds=large-adj)
  1618. self.assertEqual(temp.min_desired_rounds, small+adj)
  1619. self.assertEqual(temp.default_rounds, medium)
  1620. self.assertEqual(temp.max_desired_rounds, large-adj)
  1621. def test_has_rounds_using_w_vary_rounds_parsing(self):
  1622. """
  1623. HasRounds.using() -- vary_rounds parsing
  1624. """
  1625. # setup helpers
  1626. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1627. def parse(value):
  1628. return subcls.using(vary_rounds=value).vary_rounds
  1629. # floats should be preserved
  1630. self.assertEqual(parse(0.1), 0.1)
  1631. self.assertEqual(parse('0.1'), 0.1)
  1632. # 'xx%' should be converted to float
  1633. self.assertEqual(parse('10%'), 0.1)
  1634. # ints should be preserved
  1635. self.assertEqual(parse(1000), 1000)
  1636. self.assertEqual(parse('1000'), 1000)
  1637. # float bounds should be enforced
  1638. self.assertRaises(ValueError, parse, -0.1)
  1639. self.assertRaises(ValueError, parse, 1.1)
  1640. def test_has_rounds_using_w_vary_rounds_generation(self):
  1641. """
  1642. HasRounds.using() -- vary_rounds generation
  1643. """
  1644. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1645. def get_effective_range(cls):
  1646. seen = set(get_effective_rounds(cls) for _ in irange(1000))
  1647. return min(seen), max(seen)
  1648. def assert_rounds_range(vary_rounds, lower, upper):
  1649. temp = subcls.using(vary_rounds=vary_rounds)
  1650. seen_lower, seen_upper = get_effective_range(temp)
  1651. self.assertEqual(seen_lower, lower, "vary_rounds had wrong lower limit:")
  1652. self.assertEqual(seen_upper, upper, "vary_rounds had wrong upper limit:")
  1653. # test static
  1654. assert_rounds_range(0, medium, medium)
  1655. assert_rounds_range("0%", medium, medium)
  1656. # test absolute
  1657. assert_rounds_range(adj, medium - adj, medium + adj)
  1658. assert_rounds_range(50, max(small, medium - 50), min(large, medium + 50))
  1659. # test relative - should shift over at 50% mark
  1660. if handler.rounds_cost == "log2":
  1661. # log rounds "50%" variance should only increase/decrease by 1 cost value
  1662. assert_rounds_range("1%", medium, medium)
  1663. assert_rounds_range("49%", medium, medium)
  1664. assert_rounds_range("50%", medium - adj, medium)
  1665. else:
  1666. # for linear rounds, range is frequently so huge, won't ever see ends.
  1667. # so we just check it's within an expected range.
  1668. lower, upper = get_effective_range(subcls.using(vary_rounds="50%"))
  1669. self.assertGreaterEqual(lower, max(small, medium * 0.5))
  1670. self.assertLessEqual(lower, max(small, medium * 0.8))
  1671. self.assertGreaterEqual(upper, min(large, medium * 1.2))
  1672. self.assertLessEqual(upper, min(large, medium * 1.5))
  1673. def test_has_rounds_using_and_needs_update(self):
  1674. """
  1675. HasRounds.using() -- desired_rounds + needs_update()
  1676. """
  1677. handler, subcls, small, medium, large, adj = self._create_using_rounds_helper()
  1678. temp = subcls.using(min_desired_rounds=small+2, max_desired_rounds=large-2)
  1679. # generate some sample hashes
  1680. small_hash = self.do_stub_encrypt(subcls, rounds=small)
  1681. medium_hash = self.do_stub_encrypt(subcls, rounds=medium)
  1682. large_hash = self.do_stub_encrypt(subcls, rounds=large)
  1683. # everything should be w/in bounds for original handler
  1684. self.assertFalse(subcls.needs_update(small_hash))
  1685. self.assertFalse(subcls.needs_update(medium_hash))
  1686. self.assertFalse(subcls.needs_update(large_hash))
  1687. # small & large should require update for temp handler
  1688. self.assertTrue(temp.needs_update(small_hash))
  1689. self.assertFalse(temp.needs_update(medium_hash))
  1690. self.assertTrue(temp.needs_update(large_hash))
  1691. #===================================================================
  1692. # idents
  1693. #===================================================================
  1694. def require_many_idents(self):
  1695. handler = self.handler
  1696. if not isinstance(handler, type) or not issubclass(handler, uh.HasManyIdents):
  1697. raise self.skipTest("handler doesn't derive from HasManyIdents")
  1698. def test_30_HasManyIdents(self):
  1699. """validate HasManyIdents configuration"""
  1700. cls = self.handler
  1701. self.require_many_idents()
  1702. # check settings
  1703. self.assertTrue('ident' in cls.setting_kwds)
  1704. # check ident_values list
  1705. for value in cls.ident_values:
  1706. self.assertIsInstance(value, unicode,
  1707. "cls.ident_values must be unicode:")
  1708. self.assertTrue(len(cls.ident_values)>1,
  1709. "cls.ident_values must have 2+ elements:")
  1710. # check default_ident value
  1711. self.assertIsInstance(cls.default_ident, unicode,
  1712. "cls.default_ident must be unicode:")
  1713. self.assertTrue(cls.default_ident in cls.ident_values,
  1714. "cls.default_ident must specify member of cls.ident_values")
  1715. # check optional aliases list
  1716. if cls.ident_aliases:
  1717. for alias, ident in iteritems(cls.ident_aliases):
  1718. self.assertIsInstance(alias, unicode,
  1719. "cls.ident_aliases keys must be unicode:") # XXX: allow ints?
  1720. self.assertIsInstance(ident, unicode,
  1721. "cls.ident_aliases values must be unicode:")
  1722. self.assertTrue(ident in cls.ident_values,
  1723. "cls.ident_aliases must map to cls.ident_values members: %r" % (ident,))
  1724. # check constructor validates ident correctly.
  1725. handler = cls
  1726. hash = self.get_sample_hash()[1]
  1727. kwds = handler.parsehash(hash)
  1728. del kwds['ident']
  1729. # ... accepts good ident
  1730. handler(ident=cls.default_ident, **kwds)
  1731. # ... requires ident w/o defaults
  1732. self.assertRaises(TypeError, handler, **kwds)
  1733. # ... supplies default ident
  1734. handler(use_defaults=True, **kwds)
  1735. # ... rejects bad ident
  1736. self.assertRaises(ValueError, handler, ident='xXx', **kwds)
  1737. # TODO: check various supported idents
  1738. def test_has_many_idents_using(self):
  1739. """HasManyIdents.using() -- 'default_ident' and 'ident' keywords"""
  1740. self.require_many_idents()
  1741. # pick alt ident to test with
  1742. handler = self.handler
  1743. orig_ident = handler.default_ident
  1744. for alt_ident in handler.ident_values:
  1745. if alt_ident != orig_ident:
  1746. break
  1747. else:
  1748. raise AssertionError("expected to find alternate ident: default=%r values=%r" %
  1749. (orig_ident, handler.ident_values))
  1750. def effective_ident(cls):
  1751. cls = unwrap_handler(cls)
  1752. return cls(use_defaults=True).ident
  1753. # keep default if nothing else specified
  1754. subcls = handler.using()
  1755. self.assertEqual(subcls.default_ident, orig_ident)
  1756. # accepts alt ident
  1757. subcls = handler.using(default_ident=alt_ident)
  1758. self.assertEqual(subcls.default_ident, alt_ident)
  1759. self.assertEqual(handler.default_ident, orig_ident)
  1760. # check subcls actually *generates* default ident,
  1761. # and that we didn't affect orig handler
  1762. self.assertEqual(effective_ident(subcls), alt_ident)
  1763. self.assertEqual(effective_ident(handler), orig_ident)
  1764. # rejects bad ident
  1765. self.assertRaises(ValueError, handler.using, default_ident='xXx')
  1766. # honor 'ident' alias
  1767. subcls = handler.using(ident=alt_ident)
  1768. self.assertEqual(subcls.default_ident, alt_ident)
  1769. self.assertEqual(handler.default_ident, orig_ident)
  1770. # forbid both at same time
  1771. self.assertRaises(TypeError, handler.using, default_ident=alt_ident, ident=alt_ident)
  1772. # check ident aliases are being honored
  1773. if handler.ident_aliases:
  1774. for alias, ident in handler.ident_aliases.items():
  1775. subcls = handler.using(ident=alias)
  1776. self.assertEqual(subcls.default_ident, ident, msg="alias %r:" % alias)
  1777. #===================================================================
  1778. # password size limits
  1779. #===================================================================
  1780. def test_truncate_error_setting(self):
  1781. """
  1782. validate 'truncate_error' setting & related attributes
  1783. """
  1784. # If it doesn't have truncate_size set,
  1785. # it shouldn't support truncate_error
  1786. hasher = self.handler
  1787. if hasher.truncate_size is None:
  1788. self.assertNotIn("truncate_error", hasher.setting_kwds)
  1789. return
  1790. # if hasher defaults to silently truncating,
  1791. # it MUST NOT use .truncate_verify_reject,
  1792. # because resulting hashes wouldn't verify!
  1793. if not hasher.truncate_error:
  1794. self.assertFalse(hasher.truncate_verify_reject)
  1795. # if hasher doesn't have configurable policy,
  1796. # it must throw error by default
  1797. if "truncate_error" not in hasher.setting_kwds:
  1798. self.assertTrue(hasher.truncate_error)
  1799. return
  1800. # test value parsing
  1801. def parse_value(value):
  1802. return hasher.using(truncate_error=value).truncate_error
  1803. self.assertEqual(parse_value(None), hasher.truncate_error)
  1804. self.assertEqual(parse_value(True), True)
  1805. self.assertEqual(parse_value("true"), True)
  1806. self.assertEqual(parse_value(False), False)
  1807. self.assertEqual(parse_value("false"), False)
  1808. self.assertRaises(ValueError, parse_value, "xxx")
  1809. def test_secret_wo_truncate_size(self):
  1810. """
  1811. test no password size limits enforced (if truncate_size=None)
  1812. """
  1813. # skip if hasher has a maximum password size
  1814. hasher = self.handler
  1815. if hasher.truncate_size is not None:
  1816. self.assertGreaterEqual(hasher.truncate_size, 1)
  1817. raise self.skipTest("truncate_size is set")
  1818. # NOTE: this doesn't do an exhaustive search to verify algorithm
  1819. # doesn't have some cutoff point, it just tries
  1820. # 1024-character string, and alters the last char.
  1821. # as long as algorithm doesn't clip secret at point <1024,
  1822. # the new secret shouldn't verify.
  1823. # hash a 1024-byte secret
  1824. secret = "too many secrets" * 16
  1825. alt = "x"
  1826. hash = self.do_encrypt(secret)
  1827. # check that verify doesn't silently reject secret
  1828. # (i.e. hasher mistakenly honors .truncate_verify_reject)
  1829. verify_success = not hasher.is_disabled
  1830. self.assertEqual(self.do_verify(secret, hash), verify_success,
  1831. msg="verify rejected correct secret")
  1832. # alter last byte, should get different hash, which won't verify
  1833. alt_secret = secret[:-1] + alt
  1834. self.assertFalse(self.do_verify(alt_secret, hash),
  1835. "full password not used in digest")
  1836. def test_secret_w_truncate_size(self):
  1837. """
  1838. test password size limits raise truncate_error (if appropriate)
  1839. """
  1840. #--------------------------------------------------
  1841. # check if test is applicable
  1842. #--------------------------------------------------
  1843. handler = self.handler
  1844. truncate_size = handler.truncate_size
  1845. if not truncate_size:
  1846. raise self.skipTest("truncate_size not set")
  1847. #--------------------------------------------------
  1848. # setup vars
  1849. #--------------------------------------------------
  1850. # try to get versions w/ and w/o truncate_error set.
  1851. # set to None if policy isn't configruable
  1852. size_error_type = exc.PasswordSizeError
  1853. if "truncate_error" in handler.setting_kwds:
  1854. without_error = handler.using(truncate_error=False)
  1855. with_error = handler.using(truncate_error=True)
  1856. size_error_type = exc.PasswordTruncateError
  1857. elif handler.truncate_error:
  1858. without_error = None
  1859. with_error = handler
  1860. else:
  1861. # NOTE: this mode is currently an error in test_truncate_error_setting()
  1862. without_error = handler
  1863. with_error = None
  1864. # create some test secrets
  1865. base = "too many secrets"
  1866. alt = "x" # char that's not in base, used to mutate test secrets
  1867. long_secret = repeat_string(base, truncate_size+1)
  1868. short_secret = long_secret[:-1]
  1869. alt_long_secret = long_secret[:-1] + alt
  1870. alt_short_secret = short_secret[:-1] + alt
  1871. # init flags
  1872. short_verify_success = not handler.is_disabled
  1873. long_verify_success = short_verify_success and \
  1874. not handler.truncate_verify_reject
  1875. #--------------------------------------------------
  1876. # do tests on <truncate_size> length secret, and resulting hash.
  1877. # should pass regardless of truncate_error policy.
  1878. #--------------------------------------------------
  1879. assert without_error or with_error
  1880. for cand_hasher in [without_error, with_error]:
  1881. # create & hash string that's exactly <truncate_size> chars.
  1882. short_hash = self.do_encrypt(short_secret, handler=cand_hasher)
  1883. # check hash verifies, regardless of .truncate_verify_reject
  1884. self.assertEqual(self.do_verify(short_secret, short_hash,
  1885. handler=cand_hasher),
  1886. short_verify_success)
  1887. # changing <truncate_size-1>'th char should invalidate hash
  1888. # if this fails, means (reported) truncate_size is too large.
  1889. self.assertFalse(self.do_verify(alt_short_secret, short_hash,
  1890. handler=with_error),
  1891. "truncate_size value is too large")
  1892. # verify should truncate long secret before comparing
  1893. # (unless truncate_verify_reject is set)
  1894. self.assertEqual(self.do_verify(long_secret, short_hash,
  1895. handler=cand_hasher),
  1896. long_verify_success)
  1897. #--------------------------------------------------
  1898. # do tests on <truncate_size+1> length secret,
  1899. # w/ truncate error disabled (should silently truncate)
  1900. #--------------------------------------------------
  1901. if without_error:
  1902. # create & hash string that's exactly truncate_size+1 chars
  1903. long_hash = self.do_encrypt(long_secret, handler=without_error)
  1904. # check verifies against secret (unless truncate_verify_reject=True)
  1905. self.assertEqual(self.do_verify(long_secret, long_hash,
  1906. handler=without_error),
  1907. short_verify_success)
  1908. # check mutating last char doesn't change outcome.
  1909. # if this fails, means (reported) truncate_size is too small.
  1910. self.assertEqual(self.do_verify(alt_long_secret, long_hash,
  1911. handler=without_error),
  1912. short_verify_success)
  1913. # check short_secret verifies against this hash
  1914. # if this fails, means (reported) truncate_size is too large.
  1915. self.assertTrue(self.do_verify(short_secret, long_hash,
  1916. handler=without_error))
  1917. #--------------------------------------------------
  1918. # do tests on <truncate_size+1> length secret,
  1919. # w/ truncate error
  1920. #--------------------------------------------------
  1921. if with_error:
  1922. # with errors enabled, should forbid truncation.
  1923. err = self.assertRaises(size_error_type, self.do_encrypt,
  1924. long_secret, handler=with_error)
  1925. self.assertEqual(err.max_size, truncate_size)
  1926. #===================================================================
  1927. # password contents
  1928. #===================================================================
  1929. def test_61_secret_case_sensitive(self):
  1930. """test password case sensitivity"""
  1931. hash_insensitive = self.secret_case_insensitive is True
  1932. verify_insensitive = self.secret_case_insensitive in [True,
  1933. "verify-only"]
  1934. # test hashing lower-case verifies against lower & upper
  1935. lower = 'test'
  1936. upper = 'TEST'
  1937. h1 = self.do_encrypt(lower)
  1938. if verify_insensitive and not self.handler.is_disabled:
  1939. self.assertTrue(self.do_verify(upper, h1),
  1940. "verify() should not be case sensitive")
  1941. else:
  1942. self.assertFalse(self.do_verify(upper, h1),
  1943. "verify() should be case sensitive")
  1944. # test hashing upper-case verifies against lower & upper
  1945. h2 = self.do_encrypt(upper)
  1946. if verify_insensitive and not self.handler.is_disabled:
  1947. self.assertTrue(self.do_verify(lower, h2),
  1948. "verify() should not be case sensitive")
  1949. else:
  1950. self.assertFalse(self.do_verify(lower, h2),
  1951. "verify() should be case sensitive")
  1952. # test genhash
  1953. # XXX: 2.0: what about 'verify-only' hashes once genhash() is removed?
  1954. # won't have easy way to recreate w/ same config to see if hash differs.
  1955. # (though only hash this applies to is mssql2000)
  1956. h2 = self.do_genhash(upper, h1)
  1957. if hash_insensitive or (self.handler.is_disabled and not self.disabled_contains_salt):
  1958. self.assertEqual(h2, h1,
  1959. "genhash() should not be case sensitive")
  1960. else:
  1961. self.assertNotEqual(h2, h1,
  1962. "genhash() should be case sensitive")
  1963. def test_62_secret_border(self):
  1964. """test non-string passwords are rejected"""
  1965. hash = self.get_sample_hash()[1]
  1966. # secret=None
  1967. self.assertRaises(TypeError, self.do_encrypt, None)
  1968. self.assertRaises(TypeError, self.do_genhash, None, hash)
  1969. self.assertRaises(TypeError, self.do_verify, None, hash)
  1970. # secret=int (picked as example of entirely wrong class)
  1971. self.assertRaises(TypeError, self.do_encrypt, 1)
  1972. self.assertRaises(TypeError, self.do_genhash, 1, hash)
  1973. self.assertRaises(TypeError, self.do_verify, 1, hash)
  1974. # xxx: move to password size limits section, above?
  1975. def test_63_large_secret(self):
  1976. """test MAX_PASSWORD_SIZE is enforced"""
  1977. from passlib.exc import PasswordSizeError
  1978. from passlib.utils import MAX_PASSWORD_SIZE
  1979. secret = '.' * (1+MAX_PASSWORD_SIZE)
  1980. hash = self.get_sample_hash()[1]
  1981. err = self.assertRaises(PasswordSizeError, self.do_genhash, secret, hash)
  1982. self.assertEqual(err.max_size, MAX_PASSWORD_SIZE)
  1983. self.assertRaises(PasswordSizeError, self.do_encrypt, secret)
  1984. self.assertRaises(PasswordSizeError, self.do_verify, secret, hash)
  1985. def test_64_forbidden_chars(self):
  1986. """test forbidden characters not allowed in password"""
  1987. chars = self.forbidden_characters
  1988. if not chars:
  1989. raise self.skipTest("none listed")
  1990. base = u('stub')
  1991. if isinstance(chars, bytes):
  1992. from passlib.utils.compat import iter_byte_chars
  1993. chars = iter_byte_chars(chars)
  1994. base = base.encode("ascii")
  1995. for c in chars:
  1996. self.assertRaises(ValueError, self.do_encrypt, base + c + base)
  1997. #===================================================================
  1998. # check identify(), verify(), genhash() against test vectors
  1999. #===================================================================
  2000. def is_secret_8bit(self, secret):
  2001. secret = self.populate_context(secret, {})
  2002. return not is_ascii_safe(secret)
  2003. def expect_os_crypt_failure(self, secret):
  2004. """
  2005. check if we're expecting potential verify failure due to crypt.crypt() encoding limitation
  2006. """
  2007. if PY3 and self.backend == "os_crypt" and isinstance(secret, bytes):
  2008. try:
  2009. secret.decode("utf-8")
  2010. except UnicodeDecodeError:
  2011. return True
  2012. return False
  2013. def test_70_hashes(self):
  2014. """test known hashes"""
  2015. # sanity check
  2016. self.assertTrue(self.known_correct_hashes or self.known_correct_configs,
  2017. "test must set at least one of 'known_correct_hashes' "
  2018. "or 'known_correct_configs'")
  2019. # run through known secret/hash pairs
  2020. saw8bit = False
  2021. for secret, hash in self.iter_known_hashes():
  2022. if self.is_secret_8bit(secret):
  2023. saw8bit = True
  2024. # hash should be positively identified by handler
  2025. self.assertTrue(self.do_identify(hash),
  2026. "identify() failed to identify hash: %r" % (hash,))
  2027. # check if what we're about to do is expected to fail due to crypt.crypt() limitation.
  2028. expect_os_crypt_failure = self.expect_os_crypt_failure(secret)
  2029. try:
  2030. # secret should verify successfully against hash
  2031. self.check_verify(secret, hash, "verify() of known hash failed: "
  2032. "secret=%r, hash=%r" % (secret, hash))
  2033. # genhash() should reproduce same hash
  2034. result = self.do_genhash(secret, hash)
  2035. self.assertIsInstance(result, str,
  2036. "genhash() failed to return native string: %r" % (result,))
  2037. if self.handler.is_disabled and self.disabled_contains_salt:
  2038. continue
  2039. self.assertEqual(result, hash, "genhash() failed to reproduce "
  2040. "known hash: secret=%r, hash=%r: result=%r" %
  2041. (secret, hash, result))
  2042. except MissingBackendError:
  2043. if not expect_os_crypt_failure:
  2044. raise
  2045. # would really like all handlers to have at least one 8-bit test vector
  2046. if not saw8bit:
  2047. warn("%s: no 8-bit secrets tested" % self.__class__)
  2048. def test_71_alternates(self):
  2049. """test known alternate hashes"""
  2050. if not self.known_alternate_hashes:
  2051. raise self.skipTest("no alternate hashes provided")
  2052. for alt, secret, hash in self.known_alternate_hashes:
  2053. # hash should be positively identified by handler
  2054. self.assertTrue(self.do_identify(hash),
  2055. "identify() failed to identify alternate hash: %r" %
  2056. (hash,))
  2057. # secret should verify successfully against hash
  2058. self.check_verify(secret, alt, "verify() of known alternate hash "
  2059. "failed: secret=%r, hash=%r" % (secret, alt))
  2060. # genhash() should reproduce canonical hash
  2061. result = self.do_genhash(secret, alt)
  2062. self.assertIsInstance(result, str,
  2063. "genhash() failed to return native string: %r" % (result,))
  2064. if self.handler.is_disabled and self.disabled_contains_salt:
  2065. continue
  2066. self.assertEqual(result, hash, "genhash() failed to normalize "
  2067. "known alternate hash: secret=%r, alt=%r, hash=%r: "
  2068. "result=%r" % (secret, alt, hash, result))
  2069. def test_72_configs(self):
  2070. """test known config strings"""
  2071. # special-case handlers without settings
  2072. if not self.handler.setting_kwds:
  2073. self.assertFalse(self.known_correct_configs,
  2074. "handler should not have config strings")
  2075. raise self.skipTest("hash has no settings")
  2076. if not self.known_correct_configs:
  2077. # XXX: make this a requirement?
  2078. raise self.skipTest("no config strings provided")
  2079. # make sure config strings work (hashes in list tested in test_70)
  2080. if self.filter_config_warnings:
  2081. warnings.filterwarnings("ignore", category=PasslibHashWarning)
  2082. for config, secret, hash in self.known_correct_configs:
  2083. # config should be positively identified by handler
  2084. self.assertTrue(self.do_identify(config),
  2085. "identify() failed to identify known config string: %r" %
  2086. (config,))
  2087. # verify() should throw error for config strings.
  2088. self.assertRaises(ValueError, self.do_verify, secret, config,
  2089. __msg__="verify() failed to reject config string: %r" %
  2090. (config,))
  2091. # genhash() should reproduce hash from config.
  2092. result = self.do_genhash(secret, config)
  2093. self.assertIsInstance(result, str,
  2094. "genhash() failed to return native string: %r" % (result,))
  2095. self.assertEqual(result, hash, "genhash() failed to reproduce "
  2096. "known hash from config: secret=%r, config=%r, hash=%r: "
  2097. "result=%r" % (secret, config, hash, result))
  2098. def test_73_unidentified(self):
  2099. """test known unidentifiably-mangled strings"""
  2100. if not self.known_unidentified_hashes:
  2101. raise self.skipTest("no unidentified hashes provided")
  2102. for hash in self.known_unidentified_hashes:
  2103. # identify() should reject these
  2104. self.assertFalse(self.do_identify(hash),
  2105. "identify() incorrectly identified known unidentifiable "
  2106. "hash: %r" % (hash,))
  2107. # verify() should throw error
  2108. self.assertRaises(ValueError, self.do_verify, 'stub', hash,
  2109. __msg__= "verify() failed to throw error for unidentifiable "
  2110. "hash: %r" % (hash,))
  2111. # genhash() should throw error
  2112. self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
  2113. __msg__= "genhash() failed to throw error for unidentifiable "
  2114. "hash: %r" % (hash,))
  2115. def test_74_malformed(self):
  2116. """test known identifiable-but-malformed strings"""
  2117. if not self.known_malformed_hashes:
  2118. raise self.skipTest("no malformed hashes provided")
  2119. for hash in self.known_malformed_hashes:
  2120. # identify() should accept these
  2121. self.assertTrue(self.do_identify(hash),
  2122. "identify() failed to identify known malformed "
  2123. "hash: %r" % (hash,))
  2124. # verify() should throw error
  2125. self.assertRaises(ValueError, self.do_verify, 'stub', hash,
  2126. __msg__= "verify() failed to throw error for malformed "
  2127. "hash: %r" % (hash,))
  2128. # genhash() should throw error
  2129. self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
  2130. __msg__= "genhash() failed to throw error for malformed "
  2131. "hash: %r" % (hash,))
  2132. def test_75_foreign(self):
  2133. """test known foreign hashes"""
  2134. if self.accepts_all_hashes:
  2135. raise self.skipTest("not applicable")
  2136. if not self.known_other_hashes:
  2137. raise self.skipTest("no foreign hashes provided")
  2138. for name, hash in self.known_other_hashes:
  2139. # NOTE: most tests use default list of foreign hashes,
  2140. # so they may include ones belonging to that hash...
  2141. # hence the 'own' logic.
  2142. if name == self.handler.name:
  2143. # identify should accept these
  2144. self.assertTrue(self.do_identify(hash),
  2145. "identify() failed to identify known hash: %r" % (hash,))
  2146. # verify & genhash should NOT throw error
  2147. self.do_verify('stub', hash)
  2148. result = self.do_genhash('stub', hash)
  2149. self.assertIsInstance(result, str,
  2150. "genhash() failed to return native string: %r" % (result,))
  2151. else:
  2152. # identify should reject these
  2153. self.assertFalse(self.do_identify(hash),
  2154. "identify() incorrectly identified hash belonging to "
  2155. "%s: %r" % (name, hash))
  2156. # verify should throw error
  2157. self.assertRaises(ValueError, self.do_verify, 'stub', hash,
  2158. __msg__= "verify() failed to throw error for hash "
  2159. "belonging to %s: %r" % (name, hash,))
  2160. # genhash() should throw error
  2161. self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
  2162. __msg__= "genhash() failed to throw error for hash "
  2163. "belonging to %s: %r" % (name, hash))
  2164. def test_76_hash_border(self):
  2165. """test non-string hashes are rejected"""
  2166. #
  2167. # test hash=None is handled correctly
  2168. #
  2169. self.assertRaises(TypeError, self.do_identify, None)
  2170. self.assertRaises(TypeError, self.do_verify, 'stub', None)
  2171. # NOTE: changed in 1.7 -- previously 'None' would be accepted when config strings not supported.
  2172. self.assertRaises(TypeError, self.do_genhash, 'stub', None)
  2173. #
  2174. # test hash=int is rejected (picked as example of entirely wrong type)
  2175. #
  2176. self.assertRaises(TypeError, self.do_identify, 1)
  2177. self.assertRaises(TypeError, self.do_verify, 'stub', 1)
  2178. self.assertRaises(TypeError, self.do_genhash, 'stub', 1)
  2179. #
  2180. # test hash='' is rejected for all but the plaintext hashes
  2181. #
  2182. for hash in [u(''), b'']:
  2183. if self.accepts_all_hashes:
  2184. # then it accepts empty string as well.
  2185. self.assertTrue(self.do_identify(hash))
  2186. self.do_verify('stub', hash)
  2187. result = self.do_genhash('stub', hash)
  2188. self.check_returned_native_str(result, "genhash")
  2189. else:
  2190. # otherwise it should reject them
  2191. self.assertFalse(self.do_identify(hash),
  2192. "identify() incorrectly identified empty hash")
  2193. self.assertRaises(ValueError, self.do_verify, 'stub', hash,
  2194. __msg__="verify() failed to reject empty hash")
  2195. self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
  2196. __msg__="genhash() failed to reject empty hash")
  2197. #
  2198. # test identify doesn't throw decoding errors on 8-bit input
  2199. #
  2200. self.do_identify('\xe2\x82\xac\xc2\xa5$') # utf-8
  2201. self.do_identify('abc\x91\x00') # non-utf8
  2202. #===================================================================
  2203. # test parsehash()
  2204. #===================================================================
  2205. #: optional list of known parse hash results for hasher
  2206. known_parsehash_results = []
  2207. def require_parsehash(self):
  2208. if not hasattr(self.handler, "parsehash"):
  2209. raise SkipTest("parsehash() not implemented")
  2210. def test_70_parsehash(self):
  2211. """
  2212. parsehash()
  2213. """
  2214. # TODO: would like to enhance what this test covers
  2215. self.require_parsehash()
  2216. handler = self.handler
  2217. # calls should succeed, and return dict
  2218. hash = self.do_encrypt("stub")
  2219. result = handler.parsehash(hash)
  2220. self.assertIsInstance(result, dict)
  2221. # TODO: figure out what invariants we can reliably parse,
  2222. # or maybe make subclasses specify that?
  2223. # w/ checksum=False, should omit that key
  2224. result2 = handler.parsehash(hash, checksum=False)
  2225. correct2 = result.copy()
  2226. correct2.pop("checksum", None)
  2227. self.assertEqual(result2, correct2)
  2228. # w/ sanitize=True
  2229. # correct output should mask salt / checksum;
  2230. # but all else should be the same
  2231. result3 = handler.parsehash(hash, sanitize=True)
  2232. correct3 = result.copy()
  2233. if PY2:
  2234. # silence warning about bytes & unicode not comparing
  2235. # (sanitize may convert bytes into base64 text)
  2236. warnings.filterwarnings("ignore", ".*unequal comparison failed to convert.*",
  2237. category=UnicodeWarning)
  2238. for key in ("salt", "checksum"):
  2239. if key in result3:
  2240. self.assertNotEqual(result3[key], correct3[key])
  2241. self.assert_is_masked(result3[key])
  2242. correct3[key] = result3[key]
  2243. self.assertEqual(result3, correct3)
  2244. def assert_is_masked(self, value):
  2245. """
  2246. check value properly masked by :func:`passlib.utils.mask_value`
  2247. """
  2248. if value is None:
  2249. return
  2250. self.assertIsInstance(value, unicode)
  2251. # assumes mask_value() defaults will never show more than <show> chars (4);
  2252. # and show nothing if size less than 1/<pct> (8).
  2253. ref = value if len(value) < 8 else value[4:]
  2254. if set(ref) == set(["*"]):
  2255. return True
  2256. raise self.fail("value not masked: %r" % value)
  2257. def test_71_parsehash_results(self):
  2258. """
  2259. parsehash() -- known outputs
  2260. """
  2261. self.require_parsehash()
  2262. samples = self.known_parsehash_results
  2263. if not samples:
  2264. raise self.skipTest("no samples present")
  2265. # XXX: expand to test w/ checksum=False and/or sanitize=True?
  2266. # or read "_unsafe_settings"?
  2267. for hash, correct in self.known_parsehash_results:
  2268. result = self.handler.parsehash(hash)
  2269. self.assertEqual(result, correct, "hash=%r:" % hash)
  2270. #===================================================================
  2271. # fuzz testing
  2272. #===================================================================
  2273. def test_77_fuzz_input(self, threaded=False):
  2274. """fuzz testing -- random passwords and options
  2275. This test attempts to perform some basic fuzz testing of the hash,
  2276. based on whatever information can be found about it.
  2277. It does as much as it can within a fixed amount of time
  2278. (defaults to 1 second, but can be overridden via $PASSLIB_TEST_FUZZ_TIME).
  2279. It tests the following:
  2280. * randomly generated passwords including extended unicode chars
  2281. * randomly selected rounds values (if rounds supported)
  2282. * randomly selected salt sizes (if salts supported)
  2283. * randomly selected identifiers (if multiple found)
  2284. * runs output of selected backend against other available backends
  2285. (if any) to detect errors occurring between different backends.
  2286. * runs output against other "external" verifiers such as OS crypt()
  2287. :param report_thread_state:
  2288. if true, writes state of loop to current_thread().passlib_fuzz_state.
  2289. used to help debug multi-threaded fuzz test issues (below)
  2290. """
  2291. if self.handler.is_disabled:
  2292. raise self.skipTest("not applicable")
  2293. # gather info
  2294. from passlib.utils import tick
  2295. max_time = self.max_fuzz_time
  2296. if max_time <= 0:
  2297. raise self.skipTest("disabled by test mode")
  2298. verifiers = self.get_fuzz_verifiers(threaded=threaded)
  2299. def vname(v):
  2300. return (v.__doc__ or v.__name__).splitlines()[0]
  2301. # init rng -- using separate one for each thread
  2302. # so things are predictable for given RANDOM_TEST_SEED
  2303. # (relies on test_78_fuzz_threading() to give threads unique names)
  2304. if threaded:
  2305. thread_name = threading.current_thread().name
  2306. else:
  2307. thread_name = "fuzz test"
  2308. rng = self.getRandom(name=thread_name)
  2309. generator = self.FuzzHashGenerator(self, rng)
  2310. # do as many tests as possible for max_time seconds
  2311. log.debug("%s: %s: started; max_time=%r verifiers=%d (%s)",
  2312. self.descriptionPrefix, thread_name, max_time, len(verifiers),
  2313. ", ".join(vname(v) for v in verifiers))
  2314. start = tick()
  2315. stop = start + max_time
  2316. count = 0
  2317. while tick() <= stop:
  2318. # generate random password & options
  2319. opts = generator.generate()
  2320. secret = opts['secret']
  2321. other = opts['other']
  2322. settings = opts['settings']
  2323. ctx = opts['context']
  2324. if ctx:
  2325. settings['context'] = ctx
  2326. # create new hash
  2327. hash = self.do_encrypt(secret, **settings)
  2328. ##log.debug("fuzz test: hash=%r secret=%r other=%r",
  2329. ## hash, secret, other)
  2330. # run through all verifiers we found.
  2331. for verify in verifiers:
  2332. name = vname(verify)
  2333. result = verify(secret, hash, **ctx)
  2334. if result == "skip": # let verifiers signal lack of support
  2335. continue
  2336. assert result is True or result is False
  2337. if not result:
  2338. raise self.failureException("failed to verify against %r verifier: "
  2339. "secret=%r config=%r hash=%r" %
  2340. (name, secret, settings, hash))
  2341. # occasionally check that some other secrets WON'T verify
  2342. # against this hash.
  2343. if rng.random() < .1:
  2344. result = verify(other, hash, **ctx)
  2345. if result and result != "skip":
  2346. raise self.failureException("was able to verify wrong "
  2347. "password using %s: wrong_secret=%r real_secret=%r "
  2348. "config=%r hash=%r" % (name, other, secret, settings, hash))
  2349. count += 1
  2350. log.debug("%s: %s: done; elapsed=%r count=%r",
  2351. self.descriptionPrefix, thread_name, tick() - start, count)
  2352. def test_78_fuzz_threading(self):
  2353. """multithreaded fuzz testing -- random password & options using multiple threads
  2354. run test_77 simultaneously in multiple threads
  2355. in an attempt to detect any concurrency issues
  2356. (e.g. the bug fixed by pybcrypt 0.3)
  2357. """
  2358. self.require_TEST_MODE("full")
  2359. import threading
  2360. # check if this test should run
  2361. if self.handler.is_disabled:
  2362. raise self.skipTest("not applicable")
  2363. thread_count = self.fuzz_thread_count
  2364. if thread_count < 1 or self.max_fuzz_time <= 0:
  2365. raise self.skipTest("disabled by test mode")
  2366. # buffer to hold errors thrown by threads
  2367. failed_lock = threading.Lock()
  2368. failed = [0]
  2369. # launch <thread count> threads, all of which run
  2370. # test_77_fuzz_input(), and see if any errors get thrown.
  2371. # if hash has concurrency issues, this should reveal it.
  2372. def wrapper():
  2373. try:
  2374. self.test_77_fuzz_input(threaded=True)
  2375. except SkipTest:
  2376. pass
  2377. except:
  2378. with failed_lock:
  2379. failed[0] += 1
  2380. raise
  2381. def launch(n):
  2382. cls = type(self)
  2383. name = "Fuzz-Thread-%d ('%s:%s.%s')" % (n, cls.__module__, cls.__name__,
  2384. self._testMethodName)
  2385. thread = threading.Thread(target=wrapper, name=name)
  2386. thread.setDaemon(True)
  2387. thread.start()
  2388. return thread
  2389. threads = [launch(n) for n in irange(thread_count)]
  2390. # wait until all threads exit
  2391. timeout = self.max_fuzz_time * thread_count * 4
  2392. stalled = 0
  2393. for thread in threads:
  2394. thread.join(timeout)
  2395. if not thread.is_alive():
  2396. continue
  2397. # XXX: not sure why this is happening, main one seems 1/4 times for sun_md5_crypt
  2398. log.error("%s timed out after %f seconds", thread.name, timeout)
  2399. stalled += 1
  2400. # if any thread threw an error, raise one ourselves.
  2401. if failed[0]:
  2402. raise self.fail("%d/%d threads failed concurrent fuzz testing "
  2403. "(see error log for details)" % (failed[0], thread_count))
  2404. if stalled:
  2405. raise self.fail("%d/%d threads stalled during concurrent fuzz testing "
  2406. "(see error log for details)" % (stalled, thread_count))
  2407. #---------------------------------------------------------------
  2408. # fuzz constants & helpers
  2409. #---------------------------------------------------------------
  2410. @property
  2411. def max_fuzz_time(self):
  2412. """amount of time to spend on fuzz testing"""
  2413. value = float(os.environ.get("PASSLIB_TEST_FUZZ_TIME") or 0)
  2414. if value:
  2415. return value
  2416. elif TEST_MODE(max="quick"):
  2417. return 0
  2418. elif TEST_MODE(max="default"):
  2419. return 1
  2420. else:
  2421. return 5
  2422. @property
  2423. def fuzz_thread_count(self):
  2424. """number of threads for threaded fuzz testing"""
  2425. value = int(os.environ.get("PASSLIB_TEST_FUZZ_THREADS") or 0)
  2426. if value:
  2427. return value
  2428. elif TEST_MODE(max="quick"):
  2429. return 0
  2430. else:
  2431. return 10
  2432. #---------------------------------------------------------------
  2433. # fuzz verifiers
  2434. #---------------------------------------------------------------
  2435. #: list of custom fuzz-test verifiers (in addition to hasher itself,
  2436. #: and backend-specific wrappers of hasher). each element is
  2437. #: name of method that will return None / a verifier callable.
  2438. fuzz_verifiers = ("fuzz_verifier_default",)
  2439. def get_fuzz_verifiers(self, threaded=False):
  2440. """return list of password verifiers (including external libs)
  2441. used by fuzz testing.
  2442. verifiers should be callable with signature
  2443. ``func(password: unicode, hash: ascii str) -> ok: bool``.
  2444. """
  2445. handler = self.handler
  2446. verifiers = []
  2447. # call all methods starting with prefix in order to create
  2448. for method_name in self.fuzz_verifiers:
  2449. func = getattr(self, method_name)()
  2450. if func is not None:
  2451. verifiers.append(func)
  2452. # create verifiers for any other available backends
  2453. # NOTE: skipping this under threading test,
  2454. # since backend switching isn't threadsafe (yet)
  2455. if hasattr(handler, "backends") and TEST_MODE("full") and not threaded:
  2456. def maker(backend):
  2457. def func(secret, hash):
  2458. orig_backend = handler.get_backend()
  2459. try:
  2460. handler.set_backend(backend)
  2461. return handler.verify(secret, hash)
  2462. finally:
  2463. handler.set_backend(orig_backend)
  2464. func.__name__ = "check_" + backend + "_backend"
  2465. func.__doc__ = backend + "-backend"
  2466. return func
  2467. for backend in iter_alt_backends(handler):
  2468. verifiers.append(maker(backend))
  2469. return verifiers
  2470. def fuzz_verifier_default(self):
  2471. # test against self
  2472. def check_default(secret, hash, **ctx):
  2473. return self.do_verify(secret, hash, **ctx)
  2474. if self.backend:
  2475. check_default.__doc__ = self.backend + "-backend"
  2476. else:
  2477. check_default.__doc__ = "self"
  2478. return check_default
  2479. #---------------------------------------------------------------
  2480. # fuzz settings generation
  2481. #---------------------------------------------------------------
  2482. class FuzzHashGenerator(object):
  2483. """
  2484. helper which takes care of generating random
  2485. passwords & configuration options to test hash with.
  2486. separate from test class so we can create one per thread.
  2487. """
  2488. #==========================================================
  2489. # class attrs
  2490. #==========================================================
  2491. # alphabet for randomly generated passwords
  2492. password_alphabet = u('qwertyASDF1234<>.@*#! \u00E1\u0259\u0411\u2113')
  2493. # encoding when testing bytes
  2494. password_encoding = "utf-8"
  2495. # map of setting kwd -> method name.
  2496. # will ignore setting if method returns None.
  2497. # subclasses should make copy of dict.
  2498. settings_map = dict(rounds="random_rounds",
  2499. salt_size="random_salt_size",
  2500. ident="random_ident")
  2501. # map of context kwd -> method name.
  2502. context_map = {}
  2503. #==========================================================
  2504. # init / generation
  2505. #==========================================================
  2506. def __init__(self, test, rng):
  2507. self.test = test
  2508. self.handler = test.handler
  2509. self.rng = rng
  2510. def generate(self):
  2511. """
  2512. generate random password and options for fuzz testing.
  2513. :returns:
  2514. `(secret, other_secret, settings_kwds, context_kwds)`
  2515. """
  2516. def gendict(map):
  2517. out = {}
  2518. for key, meth in map.items():
  2519. value = getattr(self, meth)()
  2520. if value is not None:
  2521. out[key] = value
  2522. return out
  2523. secret, other = self.random_password_pair()
  2524. return dict(secret=secret,
  2525. other=other,
  2526. settings=gendict(self.settings_map),
  2527. context=gendict(self.context_map),
  2528. )
  2529. #==========================================================
  2530. # helpers
  2531. #==========================================================
  2532. def randintgauss(self, lower, upper, mu, sigma):
  2533. """generate random int w/ gauss distirbution"""
  2534. value = self.rng.normalvariate(mu, sigma)
  2535. return int(limit(value, lower, upper))
  2536. #==========================================================
  2537. # settings generation
  2538. #==========================================================
  2539. def random_rounds(self):
  2540. handler = self.handler
  2541. if not has_rounds_info(handler):
  2542. return None
  2543. default = handler.default_rounds or handler.min_rounds
  2544. lower = handler.min_rounds
  2545. if handler.rounds_cost == "log2":
  2546. upper = default
  2547. else:
  2548. upper = min(default*2, handler.max_rounds)
  2549. return self.randintgauss(lower, upper, default, default*.5)
  2550. def random_salt_size(self):
  2551. handler = self.handler
  2552. if not (has_salt_info(handler) and 'salt_size' in handler.setting_kwds):
  2553. return None
  2554. default = handler.default_salt_size
  2555. lower = handler.min_salt_size
  2556. upper = handler.max_salt_size or default*4
  2557. return self.randintgauss(lower, upper, default, default*.5)
  2558. def random_ident(self):
  2559. rng = self.rng
  2560. handler = self.handler
  2561. if 'ident' not in handler.setting_kwds or not hasattr(handler, "ident_values"):
  2562. return None
  2563. if rng.random() < .5:
  2564. return None
  2565. # resolve wrappers before reading values
  2566. handler = getattr(handler, "wrapped", handler)
  2567. return rng.choice(handler.ident_values)
  2568. #==========================================================
  2569. # fuzz password generation
  2570. #==========================================================
  2571. def random_password_pair(self):
  2572. """generate random password, and non-matching alternate password"""
  2573. secret = self.random_password()
  2574. while True:
  2575. other = self.random_password()
  2576. if self.accept_password_pair(secret, other):
  2577. break
  2578. rng = self.rng
  2579. if rng.randint(0,1):
  2580. secret = secret.encode(self.password_encoding)
  2581. if rng.randint(0,1):
  2582. other = other.encode(self.password_encoding)
  2583. return secret, other
  2584. def random_password(self):
  2585. """generate random passwords for fuzz testing"""
  2586. # occasionally try an empty password
  2587. rng = self.rng
  2588. if rng.random() < .0001:
  2589. return u('')
  2590. # check if truncate size needs to be considered
  2591. handler = self.handler
  2592. truncate_size = handler.truncate_error and handler.truncate_size
  2593. max_size = truncate_size or 999999
  2594. # pick endpoint
  2595. if max_size < 50 or rng.random() < .5:
  2596. # chance of small password (~15 chars)
  2597. size = self.randintgauss(1, min(max_size, 50), 15, 15)
  2598. else:
  2599. # otherwise large password (~70 chars)
  2600. size = self.randintgauss(50, min(max_size, 99), 70, 20)
  2601. # generate random password
  2602. result = getrandstr(rng, self.password_alphabet, size)
  2603. # trim ones that encode past truncate point.
  2604. if truncate_size and isinstance(result, unicode):
  2605. while len(result.encode("utf-8")) > truncate_size:
  2606. result = result[:-1]
  2607. return result
  2608. def accept_password_pair(self, secret, other):
  2609. """verify fuzz pair contains different passwords"""
  2610. return secret != other
  2611. #==========================================================
  2612. # eoc FuzzGenerator
  2613. #==========================================================
  2614. #===================================================================
  2615. # "disabled hasher" api
  2616. #===================================================================
  2617. def test_disable_and_enable(self):
  2618. """.disable() / .enable() methods"""
  2619. #
  2620. # setup
  2621. #
  2622. handler = self.handler
  2623. if not handler.is_disabled:
  2624. self.assertFalse(hasattr(handler, "disable"))
  2625. self.assertFalse(hasattr(handler, "enable"))
  2626. self.assertFalse(self.disabled_contains_salt)
  2627. raise self.skipTest("not applicable")
  2628. #
  2629. # disable()
  2630. #
  2631. # w/o existing hash
  2632. disabled_default = handler.disable()
  2633. self.assertIsInstance(disabled_default, str,
  2634. msg="disable() must return native string")
  2635. self.assertTrue(handler.identify(disabled_default),
  2636. msg="identify() didn't recognize disable() result: %r" % (disabled_default))
  2637. # w/ existing hash
  2638. stub = self.getRandom().choice(self.known_other_hashes)[1]
  2639. disabled_stub = handler.disable(stub)
  2640. self.assertIsInstance(disabled_stub, str,
  2641. msg="disable() must return native string")
  2642. self.assertTrue(handler.identify(disabled_stub),
  2643. msg="identify() didn't recognize disable() result: %r" % (disabled_stub))
  2644. #
  2645. # enable()
  2646. #
  2647. # w/o original hash
  2648. self.assertRaisesRegex(ValueError, "cannot restore original hash",
  2649. handler.enable, disabled_default)
  2650. # w/ original hash
  2651. try:
  2652. result = handler.enable(disabled_stub)
  2653. error = None
  2654. except ValueError as e:
  2655. result = None
  2656. error = e
  2657. if error is None:
  2658. # if supports recovery, should have returned stub (e.g. unix_disabled);
  2659. self.assertIsInstance(result, str,
  2660. msg="enable() must return native string")
  2661. self.assertEqual(result, stub)
  2662. else:
  2663. # if doesn't, should have thrown appropriate error
  2664. self.assertIsInstance(error, ValueError)
  2665. self.assertRegex("cannot restore original hash", str(error))
  2666. #
  2667. # test repeating disable() & salting state
  2668. #
  2669. # repeating disabled
  2670. disabled_default2 = handler.disable()
  2671. if self.disabled_contains_salt:
  2672. # should return new salt for each call (e.g. django_disabled)
  2673. self.assertNotEqual(disabled_default2, disabled_default)
  2674. elif error is None:
  2675. # should return same result for each hash, but unique across hashes
  2676. self.assertEqual(disabled_default2, disabled_default)
  2677. # repeating same hash ...
  2678. disabled_stub2 = handler.disable(stub)
  2679. if self.disabled_contains_salt:
  2680. # ... should return different string (if salted)
  2681. self.assertNotEqual(disabled_stub2, disabled_stub)
  2682. else:
  2683. # ... should return same string
  2684. self.assertEqual(disabled_stub2, disabled_stub)
  2685. # using different hash ...
  2686. disabled_other = handler.disable(stub + 'xxx')
  2687. if self.disabled_contains_salt or error is None:
  2688. # ... should return different string (if salted or hash encoded)
  2689. self.assertNotEqual(disabled_other, disabled_stub)
  2690. else:
  2691. # ... should return same string
  2692. self.assertEqual(disabled_other, disabled_stub)
  2693. #===================================================================
  2694. # eoc
  2695. #===================================================================
  2696. #=============================================================================
  2697. # HandlerCase mixins providing additional tests for certain hashes
  2698. #=============================================================================
  2699. class OsCryptMixin(HandlerCase):
  2700. """helper used by create_backend_case() which adds additional features
  2701. to test the os_crypt backend.
  2702. * if crypt support is missing, inserts fake crypt support to simulate
  2703. a working safe_crypt, to test passlib's codepath as fully as possible.
  2704. * extra tests to verify non-conformant crypt implementations are handled
  2705. correctly.
  2706. * check that native crypt support is detected correctly for known platforms.
  2707. """
  2708. #===================================================================
  2709. # class attrs
  2710. #===================================================================
  2711. # platforms that are known to support / not support this hash natively.
  2712. # list of (platform_regex, True|False|None) entries.
  2713. platform_crypt_support = []
  2714. #===================================================================
  2715. # instance attrs
  2716. #===================================================================
  2717. __unittest_skip = True
  2718. # force this backend
  2719. backend = "os_crypt"
  2720. # flag read by HandlerCase to detect if fake os crypt is enabled.
  2721. using_patched_crypt = False
  2722. #===================================================================
  2723. # setup
  2724. #===================================================================
  2725. def setUp(self):
  2726. assert self.backend == "os_crypt"
  2727. if not self.handler.has_backend("os_crypt"):
  2728. # XXX: currently, any tests that use this are skipped entirely! (see issue 120)
  2729. self._patch_safe_crypt()
  2730. super(OsCryptMixin, self).setUp()
  2731. @classmethod
  2732. def _get_safe_crypt_handler_backend(cls):
  2733. """
  2734. return (handler, backend) pair to use for faking crypt.crypt() support for hash.
  2735. backend will be None if none availabe.
  2736. """
  2737. # find handler that generates safe_crypt() compatible hash
  2738. handler = unwrap_handler(cls.handler)
  2739. # hack to prevent recursion issue when .has_backend() is called
  2740. handler.get_backend()
  2741. # find backend which isn't os_crypt
  2742. alt_backend = get_alt_backend(handler, "os_crypt")
  2743. return handler, alt_backend
  2744. @property
  2745. def has_os_crypt_fallback(self):
  2746. """
  2747. test if there's a fallback handler to test against if os_crypt can't support
  2748. a specified secret (may be explicitly set to False for some subclasses)
  2749. """
  2750. return self._get_safe_crypt_handler_backend()[0] is not None
  2751. def _patch_safe_crypt(self):
  2752. """if crypt() doesn't support current hash alg, this patches
  2753. safe_crypt() so that it transparently uses another one of the handler's
  2754. backends, so that we can go ahead and test as much of code path
  2755. as possible.
  2756. """
  2757. # find handler & backend
  2758. handler, alt_backend = self._get_safe_crypt_handler_backend()
  2759. if not alt_backend:
  2760. raise AssertionError("handler has no available alternate backends!")
  2761. # create subclass of handler, which we swap to an alternate backend
  2762. alt_handler = handler.using()
  2763. alt_handler.set_backend(alt_backend)
  2764. def crypt_stub(secret, hash):
  2765. hash = alt_handler.genhash(secret, hash)
  2766. assert isinstance(hash, str)
  2767. return hash
  2768. import passlib.utils as mod
  2769. self.patchAttr(mod, "_crypt", crypt_stub)
  2770. self.using_patched_crypt = True
  2771. @classmethod
  2772. def _get_skip_backend_reason(cls, backend):
  2773. """
  2774. make sure os_crypt backend is tested
  2775. when it's known os_crypt will be faked by _patch_safe_crypt()
  2776. """
  2777. assert backend == "os_crypt"
  2778. reason = super(OsCryptMixin, cls)._get_skip_backend_reason(backend)
  2779. from passlib.utils import has_crypt
  2780. if reason == cls._BACKEND_NOT_AVAILABLE and has_crypt:
  2781. if TEST_MODE("full") and cls._get_safe_crypt_handler_backend()[1]:
  2782. # in this case, _patch_safe_crypt() will monkeypatch os_crypt
  2783. # to use another backend, just so we can test os_crypt fully.
  2784. return None
  2785. else:
  2786. return "hash not supported by os crypt()"
  2787. return reason
  2788. #===================================================================
  2789. # custom tests
  2790. #===================================================================
  2791. # TODO: turn into decorator, and use mock library.
  2792. def _use_mock_crypt(self):
  2793. """
  2794. patch passlib.utils.safe_crypt() so it returns mock value for duration of test.
  2795. returns function whose .return_value controls what's returned.
  2796. this defaults to None.
  2797. """
  2798. import passlib.utils as mod
  2799. def mock_crypt(secret, config):
  2800. # let 'test' string through so _load_os_crypt_backend() will still work
  2801. if secret == "test":
  2802. return mock_crypt.__wrapped__(secret, config)
  2803. else:
  2804. return mock_crypt.return_value
  2805. mock_crypt.__wrapped__ = mod._crypt
  2806. mock_crypt.return_value = None
  2807. self.patchAttr(mod, "_crypt", mock_crypt)
  2808. return mock_crypt
  2809. def test_80_faulty_crypt(self):
  2810. """test with faulty crypt()"""
  2811. hash = self.get_sample_hash()[1]
  2812. exc_types = (exc.InternalBackendError,)
  2813. mock_crypt = self._use_mock_crypt()
  2814. def test(value):
  2815. # set safe_crypt() to return specified value, and
  2816. # make sure assertion error is raised by handler.
  2817. mock_crypt.return_value = value
  2818. self.assertRaises(exc_types, self.do_genhash, "stub", hash)
  2819. self.assertRaises(exc_types, self.do_encrypt, "stub")
  2820. self.assertRaises(exc_types, self.do_verify, "stub", hash)
  2821. test('$x' + hash[2:]) # detect wrong prefix
  2822. test(hash[:-1]) # detect too short
  2823. test(hash + 'x') # detect too long
  2824. def test_81_crypt_fallback(self):
  2825. """test per-call crypt() fallback"""
  2826. # mock up safe_crypt to return None
  2827. mock_crypt = self._use_mock_crypt()
  2828. mock_crypt.return_value = None
  2829. if self.has_os_crypt_fallback:
  2830. # handler should have a fallback to use when os_crypt backend refuses to handle secret.
  2831. h1 = self.do_encrypt("stub")
  2832. h2 = self.do_genhash("stub", h1)
  2833. self.assertEqual(h2, h1)
  2834. self.assertTrue(self.do_verify("stub", h1))
  2835. else:
  2836. # handler should give up
  2837. from passlib.exc import InternalBackendError as err_type
  2838. hash = self.get_sample_hash()[1]
  2839. self.assertRaises(err_type, self.do_encrypt, 'stub')
  2840. self.assertRaises(err_type, self.do_genhash, 'stub', hash)
  2841. self.assertRaises(err_type, self.do_verify, 'stub', hash)
  2842. @doesnt_require_backend
  2843. def test_82_crypt_support(self):
  2844. """
  2845. test platform-specific crypt() support detection
  2846. NOTE: this is mainly just a sanity check to ensure the runtime
  2847. detection is functioning correctly on some known platforms,
  2848. so that we can feel more confident it'll work right on unknown ones.
  2849. """
  2850. # skip wrapper handlers, won't ever have crypt support
  2851. if hasattr(self.handler, "orig_prefix"):
  2852. raise self.skipTest("not applicable to wrappers")
  2853. # look for first entry that matches current system
  2854. # XXX: append "/" + platform.release() to string?
  2855. # XXX: probably should rework to support rows being dicts w/ "minver" / "maxver" keys,
  2856. # instead of hack where we add major # as part of platform regex.
  2857. using_backend = not self.using_patched_crypt
  2858. name = self.handler.name
  2859. platform = sys.platform
  2860. for pattern, expected in self.platform_crypt_support:
  2861. if re.match(pattern, platform):
  2862. break
  2863. else:
  2864. raise self.skipTest("no data for %r platform (current host support = %r)" %
  2865. (platform, using_backend))
  2866. # rules can use "state=None" to signal varied support;
  2867. # e.g. platform='freebsd8' ... sha256_crypt not added until 8.3
  2868. if expected is None:
  2869. raise self.skipTest("varied support on %r platform (current host support = %r)" %
  2870. (platform, using_backend))
  2871. # compare expectation vs reality
  2872. if expected == using_backend:
  2873. pass
  2874. elif expected:
  2875. self.fail("expected %r platform would have native support for %r" %
  2876. (platform, name))
  2877. else:
  2878. self.fail("did not expect %r platform would have native support for %r" %
  2879. (platform, name))
  2880. #===================================================================
  2881. # fuzzy verified support -- add additional verifier that uses os crypt()
  2882. #===================================================================
  2883. def fuzz_verifier_crypt(self):
  2884. """test results against OS crypt()"""
  2885. # don't use this if we're faking safe_crypt (pointless test),
  2886. # or if handler is a wrapper (only original handler will be supported by os)
  2887. handler = self.handler
  2888. if self.using_patched_crypt or hasattr(handler, "wrapped"):
  2889. return None
  2890. # create a wrapper for fuzzy verified to use
  2891. from crypt import crypt
  2892. from passlib.utils import _safe_crypt_lock
  2893. encoding = self.FuzzHashGenerator.password_encoding
  2894. def check_crypt(secret, hash):
  2895. """stdlib-crypt"""
  2896. if not self.crypt_supports_variant(hash):
  2897. return "skip"
  2898. # XXX: any reason not to use safe_crypt() here? or just want to test against bare metal?
  2899. secret = to_native_str(secret, encoding)
  2900. with _safe_crypt_lock:
  2901. return crypt(secret, hash) == hash
  2902. return check_crypt
  2903. def crypt_supports_variant(self, hash):
  2904. """
  2905. fuzzy_verified_crypt() helper --
  2906. used to determine if os crypt() supports a particular hash variant.
  2907. """
  2908. return True
  2909. #===================================================================
  2910. # eoc
  2911. #===================================================================
  2912. class UserHandlerMixin(HandlerCase):
  2913. """helper for handlers w/ 'user' context kwd; mixin for HandlerCase
  2914. this overrides the HandlerCase test harness methods
  2915. so that a username is automatically inserted to hash/verify
  2916. calls. as well, passing in a pair of strings as the password
  2917. will be interpreted as (secret,user)
  2918. """
  2919. #===================================================================
  2920. # option flags
  2921. #===================================================================
  2922. default_user = "user"
  2923. requires_user = True
  2924. user_case_insensitive = False
  2925. #===================================================================
  2926. # instance attrs
  2927. #===================================================================
  2928. __unittest_skip = True
  2929. #===================================================================
  2930. # custom tests
  2931. #===================================================================
  2932. def test_80_user(self):
  2933. """test user context keyword"""
  2934. handler = self.handler
  2935. password = 'stub'
  2936. hash = handler.hash(password, user=self.default_user)
  2937. if self.requires_user:
  2938. self.assertRaises(TypeError, handler.hash, password)
  2939. self.assertRaises(TypeError, handler.genhash, password, hash)
  2940. self.assertRaises(TypeError, handler.verify, password, hash)
  2941. else:
  2942. # e.g. cisco_pix works with or without one.
  2943. handler.hash(password)
  2944. handler.genhash(password, hash)
  2945. handler.verify(password, hash)
  2946. def test_81_user_case(self):
  2947. """test user case sensitivity"""
  2948. lower = self.default_user.lower()
  2949. upper = lower.upper()
  2950. hash = self.do_encrypt('stub', context=dict(user=lower))
  2951. if self.user_case_insensitive:
  2952. self.assertTrue(self.do_verify('stub', hash, user=upper),
  2953. "user should not be case sensitive")
  2954. else:
  2955. self.assertFalse(self.do_verify('stub', hash, user=upper),
  2956. "user should be case sensitive")
  2957. def test_82_user_salt(self):
  2958. """test user used as salt"""
  2959. config = self.do_stub_encrypt()
  2960. h1 = self.do_genhash('stub', config, user='admin')
  2961. h2 = self.do_genhash('stub', config, user='admin')
  2962. self.assertEqual(h2, h1)
  2963. h3 = self.do_genhash('stub', config, user='root')
  2964. self.assertNotEqual(h3, h1)
  2965. # TODO: user size? kinda dicey, depends on algorithm.
  2966. #===================================================================
  2967. # override test helpers
  2968. #===================================================================
  2969. def populate_context(self, secret, kwds):
  2970. """insert username into kwds"""
  2971. if isinstance(secret, tuple):
  2972. secret, user = secret
  2973. elif not self.requires_user:
  2974. return secret
  2975. else:
  2976. user = self.default_user
  2977. if 'user' not in kwds:
  2978. kwds['user'] = user
  2979. return secret
  2980. #===================================================================
  2981. # modify fuzz testing
  2982. #===================================================================
  2983. class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
  2984. context_map = HandlerCase.FuzzHashGenerator.context_map.copy()
  2985. context_map.update(user="random_user")
  2986. user_alphabet = u("asdQWE123")
  2987. def random_user(self):
  2988. rng = self.rng
  2989. if not self.test.requires_user and rng.random() < .1:
  2990. return None
  2991. return getrandstr(rng, self.user_alphabet, rng.randint(2,10))
  2992. #===================================================================
  2993. # eoc
  2994. #===================================================================
  2995. class EncodingHandlerMixin(HandlerCase):
  2996. """helper for handlers w/ 'encoding' context kwd; mixin for HandlerCase
  2997. this overrides the HandlerCase test harness methods
  2998. so that an encoding can be inserted to hash/verify
  2999. calls by passing in a pair of strings as the password
  3000. will be interpreted as (secret,encoding)
  3001. """
  3002. #===================================================================
  3003. # instance attrs
  3004. #===================================================================
  3005. __unittest_skip = True
  3006. # restrict stock passwords & fuzz alphabet to latin-1,
  3007. # so different encodings can be tested safely.
  3008. stock_passwords = [
  3009. u("test"),
  3010. b"test",
  3011. u("\u00AC\u00BA"),
  3012. ]
  3013. class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
  3014. password_alphabet = u('qwerty1234<>.@*#! \u00AC')
  3015. def populate_context(self, secret, kwds):
  3016. """insert encoding into kwds"""
  3017. if isinstance(secret, tuple):
  3018. secret, encoding = secret
  3019. kwds.setdefault('encoding', encoding)
  3020. return secret
  3021. #===================================================================
  3022. # eoc
  3023. #===================================================================
  3024. #=============================================================================
  3025. # warnings helpers
  3026. #=============================================================================
  3027. class reset_warnings(warnings.catch_warnings):
  3028. """catch_warnings() wrapper which clears warning registry & filters"""
  3029. def __init__(self, reset_filter="always", reset_registry=".*", **kwds):
  3030. super(reset_warnings, self).__init__(**kwds)
  3031. self._reset_filter = reset_filter
  3032. self._reset_registry = re.compile(reset_registry) if reset_registry else None
  3033. def __enter__(self):
  3034. # let parent class archive filter state
  3035. ret = super(reset_warnings, self).__enter__()
  3036. # reset the filter to list everything
  3037. if self._reset_filter:
  3038. warnings.resetwarnings()
  3039. warnings.simplefilter(self._reset_filter)
  3040. # archive and clear the __warningregistry__ key for all modules
  3041. # that match the 'reset' pattern.
  3042. pattern = self._reset_registry
  3043. if pattern:
  3044. backup = self._orig_registry = {}
  3045. for name, mod in list(sys.modules.items()):
  3046. if mod is None or not pattern.match(name):
  3047. continue
  3048. reg = getattr(mod, "__warningregistry__", None)
  3049. if reg:
  3050. backup[name] = reg.copy()
  3051. reg.clear()
  3052. return ret
  3053. def __exit__(self, *exc_info):
  3054. # restore warning registry for all modules
  3055. pattern = self._reset_registry
  3056. if pattern:
  3057. # restore registry backup, clearing all registry entries that we didn't archive
  3058. backup = self._orig_registry
  3059. for name, mod in list(sys.modules.items()):
  3060. if mod is None or not pattern.match(name):
  3061. continue
  3062. reg = getattr(mod, "__warningregistry__", None)
  3063. if reg:
  3064. reg.clear()
  3065. orig = backup.get(name)
  3066. if orig:
  3067. if reg is None:
  3068. setattr(mod, "__warningregistry__", orig)
  3069. else:
  3070. reg.update(orig)
  3071. super(reset_warnings, self).__exit__(*exc_info)
  3072. #=============================================================================
  3073. # eof
  3074. #=============================================================================