codekingpro/portable-devtools
114k
1"""passlib.context - CryptContext implementation"""2#=============================================================================3# imports4#=============================================================================5from __future__ import with_statement6# core7import re8import logging; log = logging.getLogger(__name__)9import threading10import time11from warnings import warn12# site13# pkg14from passlib import exc15from passlib.exc import ExpectedStringError, ExpectedTypeError, PasslibConfigWarning16from passlib.registry import get_crypt_handler, _validate_handler_name17from passlib.utils import (handlers as uh, to_bytes,18 to_unicode, splitcomma,19 as_bool, timer, rng, getrandstr,20 )21from passlib.utils.binary import BASE64_CHARS22from passlib.utils.compat import (iteritems, num_types, irange,23 PY2, PY3, unicode, SafeConfigParser,24 NativeStringIO, BytesIO,25 unicode_or_bytes_types, native_string_types,26 )27from passlib.utils.decor import deprecated_method, memoized_property28# local29__all__ = [30 'CryptContext',31 'LazyCryptContext',32 'CryptPolicy',33]34 35#=============================================================================36# support37#=============================================================================38 39# private object to detect unset params40_UNSET = object()41 42def _coerce_vary_rounds(value):43 """parse vary_rounds string to percent as [0,1) float, or integer"""44 if value.endswith("%"):45 # XXX: deprecate this in favor of raw float?46 return float(value.rstrip("%"))*.0147 try:48 return int(value)49 except ValueError:50 return float(value)51 52# set of options which aren't allowed to be set via policy53_forbidden_scheme_options = set(["salt"])54 # 'salt' - not allowed since a fixed salt would defeat the purpose.55 56# dict containing funcs used to coerce strings to correct type for scheme option keys.57# NOTE: this isn't really needed any longer, since Handler.using() handles the actual parsing.58# keeping this around for now, though, since it makes context.to_dict() output cleaner.59_coerce_scheme_options = dict(60 min_rounds=int,61 max_rounds=int,62 default_rounds=int,63 vary_rounds=_coerce_vary_rounds,64 salt_size=int,65)66 67def _is_handler_registered(handler):68 """detect if handler is registered or a custom handler"""69 return get_crypt_handler(handler.name, None) is handler70 71@staticmethod72def _always_needs_update(hash, secret=None):73 """74 dummy function patched into handler.needs_update() by _CryptConfig75 when hash alg has been deprecated for context.76 """77 return True78 79#: list of keys allowed under wildcard "all" scheme w/o a security warning.80_global_settings = set(["truncate_error", "vary_rounds"])81 82#=============================================================================83# crypt policy84#=============================================================================85_preamble = ("The CryptPolicy class has been deprecated as of "86 "Passlib 1.6, and will be removed in Passlib 1.8. ")87 88class CryptPolicy(object):89 """90 .. deprecated:: 1.691 This class has been deprecated, and will be removed in Passlib 1.8.92 All of its functionality has been rolled into :class:`CryptContext`.93 94 This class previously stored the configuration options for the95 CryptContext class. In the interest of interface simplification,96 all of this class' functionality has been rolled into the CryptContext97 class itself.98 The documentation for this class is now focused on documenting how to99 migrate to the new api. Additionally, where possible, the deprecation100 warnings issued by the CryptPolicy methods will list the replacement call101 that should be used.102 103 Constructors104 ============105 CryptPolicy objects can be constructed directly using any of106 the keywords accepted by :class:`CryptContext`. Direct uses of the107 :class:`!CryptPolicy` constructor should either pass the keywords108 directly into the CryptContext constructor, or to :meth:`CryptContext.update`109 if the policy object was being used to update an existing context object.110 111 In addition to passing in keywords directly,112 CryptPolicy objects can be constructed by the following methods:113 114 .. automethod:: from_path115 .. automethod:: from_string116 .. automethod:: from_source117 .. automethod:: from_sources118 .. automethod:: replace119 120 Introspection121 =============122 All of the informational methods provided by this class have been deprecated123 by identical or similar methods in the :class:`CryptContext` class:124 125 .. automethod:: has_schemes126 .. automethod:: schemes127 .. automethod:: iter_handlers128 .. automethod:: get_handler129 .. automethod:: get_options130 .. automethod:: handler_is_deprecated131 .. automethod:: get_min_verify_time132 133 Exporting134 =========135 .. automethod:: iter_config136 .. automethod:: to_dict137 .. automethod:: to_file138 .. automethod:: to_string139 140 .. note::141 CryptPolicy are immutable.142 Use the :meth:`replace` method to mutate existing instances.143 144 .. deprecated:: 1.6145 """146 #===================================================================147 # class methods148 #===================================================================149 @classmethod150 def from_path(cls, path, section="passlib", encoding="utf-8"):151 """create a CryptPolicy instance from a local file.152 153 .. deprecated:: 1.6154 155 Creating a new CryptContext from a file, which was previously done via156 ``CryptContext(policy=CryptPolicy.from_path(path))``, can now be157 done via ``CryptContext.from_path(path)``.158 See :meth:`CryptContext.from_path` for details.159 160 Updating an existing CryptContext from a file, which was previously done161 ``context.policy = CryptPolicy.from_path(path)``, can now be162 done via ``context.load_path(path)``.163 See :meth:`CryptContext.load_path` for details.164 """165 warn(_preamble +166 "Instead of ``CryptPolicy.from_path(path)``, "167 "use ``CryptContext.from_path(path)`` "168 " or ``context.load_path(path)`` for an existing CryptContext.",169 DeprecationWarning, stacklevel=2)170 return cls(_internal_context=CryptContext.from_path(path, section,171 encoding))172 173 @classmethod174 def from_string(cls, source, section="passlib", encoding="utf-8"):175 """create a CryptPolicy instance from a string.176 177 .. deprecated:: 1.6178 179 Creating a new CryptContext from a string, which was previously done via180 ``CryptContext(policy=CryptPolicy.from_string(data))``, can now be181 done via ``CryptContext.from_string(data)``.182 See :meth:`CryptContext.from_string` for details.183 184 Updating an existing CryptContext from a string, which was previously done185 ``context.policy = CryptPolicy.from_string(data)``, can now be186 done via ``context.load(data)``.187 See :meth:`CryptContext.load` for details.188 """189 warn(_preamble +190 "Instead of ``CryptPolicy.from_string(source)``, "191 "use ``CryptContext.from_string(source)`` or "192 "``context.load(source)`` for an existing CryptContext.",193 DeprecationWarning, stacklevel=2)194 return cls(_internal_context=CryptContext.from_string(source, section,195 encoding))196 197 @classmethod198 def from_source(cls, source, _warn=True):199 """create a CryptPolicy instance from some source.200 201 this method autodetects the source type, and invokes202 the appropriate constructor automatically. it attempts203 to detect whether the source is a configuration string, a filepath,204 a dictionary, or an existing CryptPolicy instance.205 206 .. deprecated:: 1.6207 208 Create a new CryptContext, which could previously be done via209 ``CryptContext(policy=CryptPolicy.from_source(source))``, should210 now be done using an explicit method: the :class:`CryptContext`211 constructor itself, :meth:`CryptContext.from_path`,212 or :meth:`CryptContext.from_string`.213 214 Updating an existing CryptContext, which could previously be done via215 ``context.policy = CryptPolicy.from_source(source)``, should216 now be done using an explicit method: :meth:`CryptContext.update`,217 or :meth:`CryptContext.load`.218 """219 if _warn:220 warn(_preamble +221 "Instead of ``CryptPolicy.from_source()``, "222 "use ``CryptContext.from_string(path)`` "223 " or ``CryptContext.from_path(source)``, as appropriate.",224 DeprecationWarning, stacklevel=2)225 if isinstance(source, CryptPolicy):226 return source227 elif isinstance(source, dict):228 return cls(_internal_context=CryptContext(**source))229 elif not isinstance(source, (bytes,unicode)):230 raise TypeError("source must be CryptPolicy, dict, config string, "231 "or file path: %r" % (type(source),))232 elif any(c in source for c in "\n\r\t") or not source.strip(" \t./;:"):233 return cls(_internal_context=CryptContext.from_string(source))234 else:235 return cls(_internal_context=CryptContext.from_path(source))236 237 @classmethod238 def from_sources(cls, sources, _warn=True):239 """create a CryptPolicy instance by merging multiple sources.240 241 each source is interpreted as by :meth:`from_source`,242 and the results are merged together.243 244 .. deprecated:: 1.6245 Instead of using this method to merge multiple policies together,246 a :class:`CryptContext` instance should be created, and then247 the multiple sources merged together via :meth:`CryptContext.load`.248 """249 if _warn:250 warn(_preamble +251 "Instead of ``CryptPolicy.from_sources()``, "252 "use the various CryptContext constructors "253 " followed by ``context.update()``.",254 DeprecationWarning, stacklevel=2)255 if len(sources) == 0:256 raise ValueError("no sources specified")257 if len(sources) == 1:258 return cls.from_source(sources[0], _warn=False)259 kwds = {}260 for source in sources:261 kwds.update(cls.from_source(source, _warn=False)._context.to_dict(resolve=True))262 return cls(_internal_context=CryptContext(**kwds))263 264 def replace(self, *args, **kwds):265 """create a new CryptPolicy, optionally updating parts of the266 existing configuration.267 268 .. deprecated:: 1.6269 Callers of this method should :meth:`CryptContext.update` or270 :meth:`CryptContext.copy` instead.271 """272 if self._stub_policy:273 warn(_preamble + # pragma: no cover -- deprecated & unused274 "Instead of ``context.policy.replace()``, "275 "use ``context.update()`` or ``context.copy()``.",276 DeprecationWarning, stacklevel=2)277 else:278 warn(_preamble +279 "Instead of ``CryptPolicy().replace()``, "280 "create a CryptContext instance and "281 "use ``context.update()`` or ``context.copy()``.",282 DeprecationWarning, stacklevel=2)283 sources = [ self ]284 if args:285 sources.extend(args)286 if kwds:287 sources.append(kwds)288 return CryptPolicy.from_sources(sources, _warn=False)289 290 #===================================================================291 # instance attrs292 #===================================================================293 294 # internal CryptContext we're wrapping to handle everything295 # until this class is removed.296 _context = None297 298 # flag indicating this is wrapper generated by the CryptContext.policy299 # attribute, rather than one created independantly by the application.300 _stub_policy = False301 302 #===================================================================303 # init304 #===================================================================305 def __init__(self, *args, **kwds):306 context = kwds.pop("_internal_context", None)307 if context:308 assert isinstance(context, CryptContext)309 self._context = context310 self._stub_policy = kwds.pop("_stub_policy", False)311 assert not (args or kwds), "unexpected args: %r %r" % (args,kwds)312 else:313 if args:314 if len(args) != 1:315 raise TypeError("only one positional argument accepted")316 if kwds:317 raise TypeError("cannot specify positional arg and kwds")318 kwds = args[0]319 warn(_preamble +320 "Instead of constructing a CryptPolicy instance, "321 "create a CryptContext directly, or use ``context.update()`` "322 "and ``context.load()`` to reconfigure existing CryptContext "323 "instances.",324 DeprecationWarning, stacklevel=2)325 self._context = CryptContext(**kwds)326 327 #===================================================================328 # public interface for examining options329 #===================================================================330 def has_schemes(self):331 """return True if policy defines *any* schemes for use.332 333 .. deprecated:: 1.6334 applications should use ``bool(context.schemes())`` instead.335 see :meth:`CryptContext.schemes`.336 """337 if self._stub_policy:338 warn(_preamble + # pragma: no cover -- deprecated & unused339 "Instead of ``context.policy.has_schemes()``, "340 "use ``bool(context.schemes())``.",341 DeprecationWarning, stacklevel=2)342 else:343 warn(_preamble +344 "Instead of ``CryptPolicy().has_schemes()``, "345 "create a CryptContext instance and "346 "use ``bool(context.schemes())``.",347 DeprecationWarning, stacklevel=2)348 return bool(self._context.schemes())349 350 def iter_handlers(self):351 """return iterator over handlers defined in policy.352 353 .. deprecated:: 1.6354 applications should use ``context.schemes(resolve=True))`` instead.355 see :meth:`CryptContext.schemes`.356 """357 if self._stub_policy:358 warn(_preamble +359 "Instead of ``context.policy.iter_handlers()``, "360 "use ``context.schemes(resolve=True)``.",361 DeprecationWarning, stacklevel=2)362 else:363 warn(_preamble +364 "Instead of ``CryptPolicy().iter_handlers()``, "365 "create a CryptContext instance and "366 "use ``context.schemes(resolve=True)``.",367 DeprecationWarning, stacklevel=2)368 return self._context.schemes(resolve=True, unconfigured=True)369 370 def schemes(self, resolve=False):371 """return list of schemes defined in policy.372 373 .. deprecated:: 1.6374 applications should use :meth:`CryptContext.schemes` instead.375 """376 if self._stub_policy:377 warn(_preamble + # pragma: no cover -- deprecated & unused378 "Instead of ``context.policy.schemes()``, "379 "use ``context.schemes()``.",380 DeprecationWarning, stacklevel=2)381 else:382 warn(_preamble +383 "Instead of ``CryptPolicy().schemes()``, "384 "create a CryptContext instance and "385 "use ``context.schemes()``.",386 DeprecationWarning, stacklevel=2)387 return list(self._context.schemes(resolve=resolve, unconfigured=True))388 389 def get_handler(self, name=None, category=None, required=False):390 """return handler as specified by name, or default handler.391 392 .. deprecated:: 1.6393 applications should use :meth:`CryptContext.handler` instead,394 though note that the ``required`` keyword has been removed,395 and the new method will always act as if ``required=True``.396 """397 if self._stub_policy:398 warn(_preamble +399 "Instead of ``context.policy.get_handler()``, "400 "use ``context.handler()``.",401 DeprecationWarning, stacklevel=2)402 else:403 warn(_preamble +404 "Instead of ``CryptPolicy().get_handler()``, "405 "create a CryptContext instance and "406 "use ``context.handler()``.",407 DeprecationWarning, stacklevel=2)408 # CryptContext.handler() doesn't support required=False,409 # so wrapping it in try/except410 try:411 return self._context.handler(name, category, unconfigured=True)412 except KeyError:413 if required:414 raise415 else:416 return None417 418 def get_min_verify_time(self, category=None):419 """get min_verify_time setting for policy.420 421 .. deprecated:: 1.6422 min_verify_time option will be removed entirely in passlib 1.8423 424 .. versionchanged:: 1.7425 this method now always returns the value automatically426 calculated by :meth:`CryptContext.min_verify_time`,427 any value specified by policy is ignored.428 """429 warn("get_min_verify_time() and min_verify_time option is deprecated and ignored, "430 "and will be removed in Passlib 1.8", DeprecationWarning,431 stacklevel=2)432 return 0433 434 def get_options(self, name, category=None):435 """return dictionary of options specific to a given handler.436 437 .. deprecated:: 1.6438 this method has no direct replacement in the 1.6 api, as there439 is not a clearly defined use-case. however, examining the output of440 :meth:`CryptContext.to_dict` should serve as the closest alternative.441 """442 # XXX: might make a public replacement, but need more study of the use cases.443 if self._stub_policy:444 warn(_preamble + # pragma: no cover -- deprecated & unused445 "``context.policy.get_options()`` will no longer be available.",446 DeprecationWarning, stacklevel=2)447 else:448 warn(_preamble +449 "``CryptPolicy().get_options()`` will no longer be available.",450 DeprecationWarning, stacklevel=2)451 if hasattr(name, "name"):452 name = name.name453 return self._context._config._get_record_options_with_flag(name, category)[0]454 455 def handler_is_deprecated(self, name, category=None):456 """check if handler has been deprecated by policy.457 458 .. deprecated:: 1.6459 this method has no direct replacement in the 1.6 api, as there460 is not a clearly defined use-case. however, examining the output of461 :meth:`CryptContext.to_dict` should serve as the closest alternative.462 """463 # XXX: might make a public replacement, but need more study of the use cases.464 if self._stub_policy:465 warn(_preamble +466 "``context.policy.handler_is_deprecated()`` will no longer be available.",467 DeprecationWarning, stacklevel=2)468 else:469 warn(_preamble +470 "``CryptPolicy().handler_is_deprecated()`` will no longer be available.",471 DeprecationWarning, stacklevel=2)472 if hasattr(name, "name"):473 name = name.name474 return self._context.handler(name, category).deprecated475 476 #===================================================================477 # serialization478 #===================================================================479 480 def iter_config(self, ini=False, resolve=False):481 """iterate over key/value pairs representing the policy object.482 483 .. deprecated:: 1.6484 applications should use :meth:`CryptContext.to_dict` instead.485 """486 if self._stub_policy:487 warn(_preamble + # pragma: no cover -- deprecated & unused488 "Instead of ``context.policy.iter_config()``, "489 "use ``context.to_dict().items()``.",490 DeprecationWarning, stacklevel=2)491 else:492 warn(_preamble +493 "Instead of ``CryptPolicy().iter_config()``, "494 "create a CryptContext instance and "495 "use ``context.to_dict().items()``.",496 DeprecationWarning, stacklevel=2)497 # hacked code that renders keys & values in manner that approximates498 # old behavior. context.to_dict() is much cleaner.499 context = self._context500 if ini:501 def render_key(key):502 return context._render_config_key(key).replace("__", ".")503 def render_value(value):504 if isinstance(value, (list,tuple)):505 value = ", ".join(value)506 return value507 resolve = False508 else:509 render_key = context._render_config_key510 render_value = lambda value: value511 return (512 (render_key(key), render_value(value))513 for key, value in context._config.iter_config(resolve)514 )515 516 def to_dict(self, resolve=False):517 """export policy object as dictionary of options.518 519 .. deprecated:: 1.6520 applications should use :meth:`CryptContext.to_dict` instead.521 """522 if self._stub_policy:523 warn(_preamble +524 "Instead of ``context.policy.to_dict()``, "525 "use ``context.to_dict()``.",526 DeprecationWarning, stacklevel=2)527 else:528 warn(_preamble +529 "Instead of ``CryptPolicy().to_dict()``, "530 "create a CryptContext instance and "531 "use ``context.to_dict()``.",532 DeprecationWarning, stacklevel=2)533 return self._context.to_dict(resolve)534 535 def to_file(self, stream, section="passlib"): # pragma: no cover -- deprecated & unused536 """export policy to file.537 538 .. deprecated:: 1.6539 applications should use :meth:`CryptContext.to_string` instead,540 and then write the output to a file as desired.541 """542 if self._stub_policy:543 warn(_preamble +544 "Instead of ``context.policy.to_file(stream)``, "545 "use ``stream.write(context.to_string())``.",546 DeprecationWarning, stacklevel=2)547 else:548 warn(_preamble +549 "Instead of ``CryptPolicy().to_file(stream)``, "550 "create a CryptContext instance and "551 "use ``stream.write(context.to_string())``.",552 DeprecationWarning, stacklevel=2)553 out = self._context.to_string(section=section)554 if PY2:555 out = out.encode("utf-8")556 stream.write(out)557 558 def to_string(self, section="passlib", encoding=None):559 """export policy to file.560 561 .. deprecated:: 1.6562 applications should use :meth:`CryptContext.to_string` instead.563 """564 if self._stub_policy:565 warn(_preamble + # pragma: no cover -- deprecated & unused566 "Instead of ``context.policy.to_string()``, "567 "use ``context.to_string()``.",568 DeprecationWarning, stacklevel=2)569 else:570 warn(_preamble +571 "Instead of ``CryptPolicy().to_string()``, "572 "create a CryptContext instance and "573 "use ``context.to_string()``.",574 DeprecationWarning, stacklevel=2)575 out = self._context.to_string(section=section)576 if encoding:577 out = out.encode(encoding)578 return out579 580 #===================================================================581 # eoc582 #===================================================================583 584#=============================================================================585# _CryptConfig helper class586#=============================================================================587class _CryptConfig(object):588 """parses, validates, and stores CryptContext config589 590 this is a helper used internally by CryptContext to handle591 parsing, validation, and serialization of its config options.592 split out from the main class, but not made public since593 that just complicates interface too much (c.f. CryptPolicy)594 595 :arg source: config as dict mapping ``(cat,scheme,option) -> value``596 """597 #===================================================================598 # instance attrs599 #===================================================================600 601 # triple-nested dict which maps scheme -> category -> key -> value,602 # storing all hash-specific options603 _scheme_options = None604 605 # double-nested dict which maps key -> category -> value606 # storing all CryptContext options607 _context_options = None608 609 # tuple of handler objects610 handlers = None611 612 # tuple of scheme objects in same order as handlers613 schemes = None614 615 # tuple of categories in alphabetical order (not including None)616 categories = None617 618 # set of all context keywords used by active schemes619 context_kwds = None620 621 # dict mapping category -> default scheme622 _default_schemes = None623 624 # dict mapping (scheme, category) -> custom handler625 _records = None626 627 # dict mapping category -> list of custom handler instances for that category,628 # in order of schemes(). populated on demand by _get_record_list()629 _record_lists = None630 631 #===================================================================632 # constructor633 #===================================================================634 def __init__(self, source):635 self._init_scheme_list(source.get((None,None,"schemes")))636 self._init_options(source)637 self._init_default_schemes()638 self._init_records()639 640 def _init_scheme_list(self, data):641 """initialize .handlers and .schemes attributes"""642 handlers = []643 schemes = []644 if isinstance(data, native_string_types):645 data = splitcomma(data)646 for elem in data or ():647 # resolve elem -> handler & scheme648 if hasattr(elem, "name"):649 handler = elem650 scheme = handler.name651 _validate_handler_name(scheme)652 elif isinstance(elem, native_string_types):653 handler = get_crypt_handler(elem)654 scheme = handler.name655 else:656 raise TypeError("scheme must be name or CryptHandler, "657 "not %r" % type(elem))658 659 # check scheme name isn't already in use660 if scheme in schemes:661 raise KeyError("multiple handlers with same name: %r" %662 (scheme,))663 664 # add to handler list665 handlers.append(handler)666 schemes.append(scheme)667 668 self.handlers = tuple(handlers)669 self.schemes = tuple(schemes)670 671 #===================================================================672 # lowlevel options673 #===================================================================674 675 #---------------------------------------------------------------676 # init lowlevel option storage677 #---------------------------------------------------------------678 def _init_options(self, source):679 """load config dict into internal representation,680 and init .categories attr681 """682 # prepare dicts & locals683 norm_scheme_option = self._norm_scheme_option684 norm_context_option = self._norm_context_option685 self._scheme_options = scheme_options = {}686 self._context_options = context_options = {}687 categories = set()688 689 # load source config into internal storage690 for (cat, scheme, key), value in iteritems(source):691 categories.add(cat)692 explicit_scheme = scheme693 if not cat and not scheme and key in _global_settings:694 # going forward, not using "<cat>__all__<key>" format. instead...695 # whitelisting set of keys which should be passed to (all) schemes,696 # rather than passed to the CryptContext itself697 scheme = "all"698 if scheme:699 # normalize scheme option700 key, value = norm_scheme_option(key, value)701 702 # e.g. things like "min_rounds" should never be set cross-scheme703 # this will be fatal under 2.0.704 if scheme == "all" and key not in _global_settings:705 warn("The '%s' option should be configured per-algorithm, and not set "706 "globally in the context; This will be an error in Passlib 2.0" %707 (key,), PasslibConfigWarning)708 709 # this scheme is going away in 2.0;710 # but most keys deserve an extra warning since it impacts security.711 if explicit_scheme == "all":712 warn("The 'all' scheme is deprecated as of Passlib 1.7, "713 "and will be removed in Passlib 2.0; Please configure "714 "options on a per-algorithm basis.", DeprecationWarning)715 716 # store in scheme_options717 # map structure: scheme_options[scheme][category][key] = value718 try:719 category_map = scheme_options[scheme]720 except KeyError:721 scheme_options[scheme] = {cat: {key: value}}722 else:723 try:724 option_map = category_map[cat]725 except KeyError:726 category_map[cat] = {key: value}727 else:728 option_map[key] = value729 else:730 # normalize context option731 if cat and key == "schemes":732 raise KeyError("'schemes' context option is not allowed "733 "per category")734 key, value = norm_context_option(cat, key, value)735 if key == "min_verify_time": # ignored in 1.7, to be removed in 1.8736 continue737 738 # store in context_options739 # map structure: context_options[key][category] = value740 try:741 category_map = context_options[key]742 except KeyError:743 context_options[key] = {cat: value}744 else:745 category_map[cat] = value746 747 # store list of configured categories748 categories.discard(None)749 self.categories = tuple(sorted(categories))750 751 def _norm_scheme_option(self, key, value):752 # check for invalid options753 if key in _forbidden_scheme_options:754 raise KeyError("%r option not allowed in CryptContext "755 "configuration" % (key,))756 # coerce strings for certain fields (e.g. min_rounds uses ints)757 if isinstance(value, native_string_types):758 func = _coerce_scheme_options.get(key)759 if func:760 value = func(value)761 return key, value762 763 def _norm_context_option(self, cat, key, value):764 schemes = self.schemes765 if key == "default":766 if hasattr(value, "name"):767 value = value.name768 elif not isinstance(value, native_string_types):769 raise ExpectedTypeError(value, "str", "default")770 if schemes and value not in schemes:771 raise KeyError("default scheme not found in policy")772 elif key == "deprecated":773 if isinstance(value, native_string_types):774 value = splitcomma(value)775 elif not isinstance(value, (list,tuple)):776 raise ExpectedTypeError(value, "str or seq", "deprecated")777 if 'auto' in value:778 # XXX: have any statements been made about when this is default?779 # should do it in 1.8 at latest.780 if len(value) > 1:781 raise ValueError("cannot list other schemes if "782 "``deprecated=['auto']`` is used")783 elif schemes:784 # make sure list of deprecated schemes is subset of configured schemes785 for scheme in value:786 if not isinstance(scheme, native_string_types):787 raise ExpectedTypeError(value, "str", "deprecated element")788 if scheme not in schemes:789 raise KeyError("deprecated scheme not found "790 "in policy: %r" % (scheme,))791 elif key == "min_verify_time":792 warn("'min_verify_time' was deprecated in Passlib 1.6, is "793 "ignored in 1.7, and will be removed in 1.8",794 DeprecationWarning)795 elif key == "harden_verify":796 warn("'harden_verify' is deprecated & ignored as of Passlib 1.7.1, "797 " and will be removed in 1.8",798 DeprecationWarning)799 elif key != "schemes":800 raise KeyError("unknown CryptContext keyword: %r" % (key,))801 return key, value802 803 #---------------------------------------------------------------804 # reading context options805 #---------------------------------------------------------------806 def get_context_optionmap(self, key, _default={}):807 """return dict mapping category->value for specific context option.808 809 .. warning:: treat return value as readonly!810 """811 return self._context_options.get(key, _default)812 813 def get_context_option_with_flag(self, category, key):814 """return value of specific option, handling category inheritance.815 also returns flag indicating whether value is category-specific.816 """817 try:818 category_map = self._context_options[key]819 except KeyError:820 return None, False821 value = category_map.get(None)822 if category:823 try:824 alt = category_map[category]825 except KeyError:826 pass827 else:828 if value is None or alt != value:829 return alt, True830 return value, False831 832 #---------------------------------------------------------------833 # reading scheme options834 #---------------------------------------------------------------835 def _get_scheme_optionmap(self, scheme, category, default={}):836 """return all options for (scheme,category) combination837 838 .. warning:: treat return value as readonly!839 """840 try:841 return self._scheme_options[scheme][category]842 except KeyError:843 return default844 845 def get_base_handler(self, scheme):846 return self.handlers[self.schemes.index(scheme)]847 848 @staticmethod849 def expand_settings(handler):850 setting_kwds = handler.setting_kwds851 if 'rounds' in handler.setting_kwds:852 # XXX: historically this extras won't be listed in setting_kwds853 setting_kwds += uh.HasRounds.using_rounds_kwds854 return setting_kwds855 856 # NOTE: this is only used by _get_record_options_with_flag()...857 def get_scheme_options_with_flag(self, scheme, category):858 """return composite dict of all options set for scheme.859 includes options inherited from 'all' and from default category.860 result can be modified.861 returns (kwds, has_cat_specific_options)862 """863 # start out with copy of global options864 get_optionmap = self._get_scheme_optionmap865 kwds = get_optionmap("all", None).copy()866 has_cat_options = False867 868 # add in category-specific global options869 if category:870 defkwds = kwds.copy() # <-- used to detect category-specific options871 kwds.update(get_optionmap("all", category))872 873 # filter out global settings not supported by handler874 allowed_settings = self.expand_settings(self.get_base_handler(scheme))875 for key in set(kwds).difference(allowed_settings):876 kwds.pop(key)877 if category:878 for key in set(defkwds).difference(allowed_settings):879 defkwds.pop(key)880 881 # add in default options for scheme882 other = get_optionmap(scheme, None)883 kwds.update(other)884 885 # load category-specific options for scheme886 if category:887 defkwds.update(other)888 kwds.update(get_optionmap(scheme, category))889 890 # compare default category options to see if there's anything891 # category-specific892 if kwds != defkwds:893 has_cat_options = True894 895 return kwds, has_cat_options896 897 #===================================================================898 # deprecated & default schemes899 #===================================================================900 def _init_default_schemes(self):901 """initialize maps containing default scheme for each category.902 903 have to do this after _init_options(), since the default scheme904 is affected by the list of deprecated schemes.905 """906 # init maps & locals907 get_optionmap = self.get_context_optionmap908 default_map = self._default_schemes = get_optionmap("default").copy()909 dep_map = get_optionmap("deprecated")910 schemes = self.schemes911 if not schemes:912 return913 914 # figure out default scheme915 deps = dep_map.get(None) or ()916 default = default_map.get(None)917 if not default:918 for scheme in schemes:919 if scheme not in deps:920 default_map[None] = scheme921 break922 else:923 raise ValueError("must have at least one non-deprecated scheme")924 elif default in deps:925 raise ValueError("default scheme cannot be deprecated")926 927 # figure out per-category default schemes,928 for cat in self.categories:929 cdeps = dep_map.get(cat, deps)930 cdefault = default_map.get(cat, default)931 if not cdefault:932 for scheme in schemes:933 if scheme not in cdeps:934 default_map[cat] = scheme935 break936 else:937 raise ValueError("must have at least one non-deprecated "938 "scheme for %r category" % cat)939 elif cdefault in cdeps:940 raise ValueError("default scheme for %r category "941 "cannot be deprecated" % cat)942 943 def default_scheme(self, category):944 """return default scheme for specific category"""945 defaults = self._default_schemes946 try:947 return defaults[category]948 except KeyError:949 pass950 if not self.schemes:951 raise KeyError("no hash schemes configured for this "952 "CryptContext instance")953 return defaults[None]954 955 def is_deprecated_with_flag(self, scheme, category):956 """is scheme deprecated under particular category?"""957 depmap = self.get_context_optionmap("deprecated")958 def test(cat):959 source = depmap.get(cat, depmap.get(None))960 if source is None:961 return None962 elif 'auto' in source:963 return scheme != self.default_scheme(cat)964 else:965 return scheme in source966 value = test(None) or False967 if category:968 alt = test(category)969 if alt is not None and value != alt:970 return alt, True971 return value, False972 973 #===================================================================974 # CryptRecord objects975 #===================================================================976 def _init_records(self):977 # NOTE: this step handles final validation of settings,978 # checking for violations against handler's internal invariants.979 # this is why we create all the records now,980 # so CryptContext throws error immediately rather than later.981 self._record_lists = {}982 records = self._records = {}983 all_context_kwds = self.context_kwds = set()984 get_options = self._get_record_options_with_flag985 categories = (None,) + self.categories986 for handler in self.handlers:987 scheme = handler.name988 all_context_kwds.update(handler.context_kwds)989 for cat in categories:990 kwds, has_cat_options = get_options(scheme, cat)991 if cat is None or has_cat_options:992 records[scheme, cat] = self._create_record(handler, cat, **kwds)993 # NOTE: if handler has no category-specific opts, get_record()994 # will automatically use the default category's record.995 # NOTE: default records for specific category stored under the996 # key (None,category); these are populated on-demand by get_record().997 998 @staticmethod999 def _create_record(handler, category=None, deprecated=False, **settings):1000 # create custom handler if needed.1001 try:1002 # XXX: relaxed=True is mostly here to retain backwards-compat behavior.1003 # could make this optional flag in future.1004 subcls = handler.using(relaxed=True, **settings)1005 except TypeError as err:1006 m = re.match(r".* unexpected keyword argument '(.*)'$", str(err))1007 if m and m.group(1) in settings:1008 # translate into KeyError, for backwards compat.1009 # XXX: push this down to GenericHandler.using() implementation?1010 key = m.group(1)1011 raise KeyError("keyword not supported by %s handler: %r" %1012 (handler.name, key))1013 raise1014 1015 # using private attrs to store some extra metadata in custom handler1016 assert subcls is not handler, "expected unique variant of handler"1017 ##subcls._Context__category = category1018 subcls._Context__orig_handler = handler1019 subcls.deprecated = deprecated # attr reserved for this purpose1020 return subcls1021 1022 def _get_record_options_with_flag(self, scheme, category):1023 """return composite dict of options for given scheme + category.1024 1025 this is currently a private method, though some variant1026 of its output may eventually be made public.1027 1028 given a scheme & category, it returns two things:1029 a set of all the keyword options to pass to :meth:`_create_record`,1030 and a bool flag indicating whether any of these options1031 were specific to the named category. if this flag is false,1032 the options are identical to the options for the default category.1033 1034 the options dict includes all the scheme-specific settings,1035 as well as optional *deprecated* keyword.1036 """1037 # get scheme options1038 kwds, has_cat_options = self.get_scheme_options_with_flag(scheme, category)1039 1040 # throw in deprecated flag1041 value, not_inherited = self.is_deprecated_with_flag(scheme, category)1042 if value:1043 kwds['deprecated'] = True1044 if not_inherited:1045 has_cat_options = True1046 1047 return kwds, has_cat_options1048 1049 def get_record(self, scheme, category):1050 """return record for specific scheme & category (cached)"""1051 # NOTE: this is part of the critical path shared by1052 # all of CryptContext's PasswordHash methods,1053 # hence all the caching and error checking.1054 1055 # quick lookup in cache1056 try:1057 return self._records[scheme, category]1058 except KeyError:1059 pass1060 1061 # type check1062 if category is not None and not isinstance(category, native_string_types):1063 if PY2 and isinstance(category, unicode):1064 # for compatibility with unicode-centric py2 apps1065 return self.get_record(scheme, category.encode("utf-8"))1066 raise ExpectedTypeError(category, "str or None", "category")1067 if scheme is not None and not isinstance(scheme, native_string_types):1068 raise ExpectedTypeError(scheme, "str or None", "scheme")1069 1070 # if scheme=None,1071 # use record for category's default scheme, and cache result.1072 if not scheme:1073 default = self.default_scheme(category)1074 assert default1075 record = self._records[None, category] = self.get_record(default,1076 category)1077 return record1078 1079 # if no record for (scheme, category),1080 # use record for (scheme, None), and cache result.1081 if category:1082 try:1083 cache = self._records1084 record = cache[scheme, category] = cache[scheme, None]1085 return record1086 except KeyError:1087 pass1088 1089 # scheme not found in configuration for default category1090 raise KeyError("crypt algorithm not found in policy: %r" % (scheme,))1091 1092 def _get_record_list(self, category=None):1093 """return list of records for category (cached)1094 1095 this is an internal helper used only by identify_record()1096 """1097 # type check of category - handled by _get_record()1098 # quick lookup in cache1099 try:1100 return self._record_lists[category]1101 except KeyError:1102 pass1103 # cache miss - build list from scratch1104 value = self._record_lists[category] = [1105 self.get_record(scheme, category)1106 for scheme in self.schemes1107 ]1108 return value1109 1110 def identify_record(self, hash, category, required=True):1111 """internal helper to identify appropriate custom handler for hash"""1112 # NOTE: this is part of the critical path shared by1113 # all of CryptContext's PasswordHash methods,1114 # hence all the caching and error checking.1115 # FIXME: if multiple hashes could match (e.g. lmhash vs nthash)1116 # this will only return first match. might want to do something1117 # about this in future, but for now only hashes with1118 # unique identifiers will work properly in a CryptContext.1119 # XXX: if all handlers have a unique prefix (e.g. all are MCF / LDAP),1120 # could use dict-lookup to speed up this search.1121 if not isinstance(hash, unicode_or_bytes_types):1122 raise ExpectedStringError(hash, "hash")1123 # type check of category - handled by _get_record_list()1124 for record in self._get_record_list(category):1125 if record.identify(hash):1126 return record1127 if not required:1128 return None1129 elif not self.schemes:1130 raise KeyError("no crypt algorithms supported")1131 else:1132 raise exc.UnknownHashError("hash could not be identified")1133 1134 @memoized_property1135 def disabled_record(self):1136 for record in self._get_record_list(None):1137 if record.is_disabled:1138 return record1139 raise RuntimeError("no disabled hasher present "1140 "(perhaps add 'unix_disabled' to list of schemes?)")1141 1142 #===================================================================1143 # serialization1144 #===================================================================1145 def iter_config(self, resolve=False):1146 """regenerate original config.1147 1148 this is an iterator which yields ``(cat,scheme,option),value`` items,1149 in the order they generally appear inside an INI file.1150 if interpreted as a dictionary, it should match the original1151 keywords passed to the CryptContext (aside from any canonization).1152 1153 it's mainly used as the internal backend for most of the public1154 serialization methods.1155 """1156 # grab various bits of data1157 scheme_options = self._scheme_options1158 context_options = self._context_options1159 scheme_keys = sorted(scheme_options)1160 context_keys = sorted(context_options)1161 1162 # write loaded schemes (may differ from 'schemes' local var)1163 if 'schemes' in context_keys:1164 context_keys.remove("schemes")1165 value = self.handlers if resolve else self.schemes1166 if value:1167 yield (None, None, "schemes"), list(value)1168 1169 # then run through config for each user category1170 for cat in (None,) + self.categories:1171 1172 # write context options1173 for key in context_keys:1174 try:1175 value = context_options[key][cat]1176 except KeyError:1177 pass1178 else:1179 if isinstance(value, list):1180 value = list(value)1181 yield (cat, None, key), value1182 1183 # write per-scheme options for all schemes.1184 for scheme in scheme_keys:1185 try:1186 kwds = scheme_options[scheme][cat]1187 except KeyError:1188 pass1189 else:1190 for key in sorted(kwds):1191 yield (cat, scheme, key), kwds[key]1192 1193 #===================================================================1194 # eoc1195 #===================================================================1196 1197#=============================================================================1198# main CryptContext class1199#=============================================================================1200class CryptContext(object):