|
- """passlib.context - CryptContext implementation"""
- #=============================================================================
- # imports
- #=============================================================================
- from __future__ import with_statement
- # core
- import re
- import logging; log = logging.getLogger(__name__)
- import threading
- import time
- from warnings import warn
- # site
- # pkg
- from passlib.exc import ExpectedStringError, ExpectedTypeError, PasslibConfigWarning
- from passlib.registry import get_crypt_handler, _validate_handler_name
- from passlib.utils import (handlers as uh, to_bytes,
- to_unicode, splitcomma,
- as_bool, timer, rng, getrandstr,
- )
- from passlib.utils.binary import BASE64_CHARS
- from passlib.utils.compat import (iteritems, num_types, irange,
- PY2, PY3, unicode, SafeConfigParser,
- NativeStringIO, BytesIO,
- unicode_or_bytes_types, native_string_types,
- )
- from passlib.utils.decor import deprecated_method, memoized_property
- # local
- __all__ = [
- 'CryptContext',
- 'LazyCryptContext',
- 'CryptPolicy',
- ]
-
- #=============================================================================
- # support
- #=============================================================================
-
- # private object to detect unset params
- _UNSET = object()
-
- def _coerce_vary_rounds(value):
- """parse vary_rounds string to percent as [0,1) float, or integer"""
- if value.endswith("%"):
- # XXX: deprecate this in favor of raw float?
- return float(value.rstrip("%"))*.01
- try:
- return int(value)
- except ValueError:
- return float(value)
-
- # set of options which aren't allowed to be set via policy
- _forbidden_scheme_options = set(["salt"])
- # 'salt' - not allowed since a fixed salt would defeat the purpose.
-
- # dict containing funcs used to coerce strings to correct type for scheme option keys.
- # NOTE: this isn't really needed any longer, since Handler.using() handles the actual parsing.
- # keeping this around for now, though, since it makes context.to_dict() output cleaner.
- _coerce_scheme_options = dict(
- min_rounds=int,
- max_rounds=int,
- default_rounds=int,
- vary_rounds=_coerce_vary_rounds,
- salt_size=int,
- )
-
- def _is_handler_registered(handler):
- """detect if handler is registered or a custom handler"""
- return get_crypt_handler(handler.name, None) is handler
-
- @staticmethod
- def _always_needs_update(hash, secret=None):
- """
- dummy function patched into handler.needs_update() by _CryptConfig
- when hash alg has been deprecated for context.
- """
- return True
-
- #: list of keys allowed under wildcard "all" scheme w/o a security warning.
- _global_settings = set(["truncate_error", "vary_rounds"])
-
- #=============================================================================
- # crypt policy
- #=============================================================================
- _preamble = ("The CryptPolicy class has been deprecated as of "
- "Passlib 1.6, and will be removed in Passlib 1.8. ")
-
- class CryptPolicy(object):
- """
- .. deprecated:: 1.6
- This class has been deprecated, and will be removed in Passlib 1.8.
- All of its functionality has been rolled into :class:`CryptContext`.
-
- This class previously stored the configuration options for the
- CryptContext class. In the interest of interface simplification,
- all of this class' functionality has been rolled into the CryptContext
- class itself.
- The documentation for this class is now focused on documenting how to
- migrate to the new api. Additionally, where possible, the deprecation
- warnings issued by the CryptPolicy methods will list the replacement call
- that should be used.
-
- Constructors
- ============
- CryptPolicy objects can be constructed directly using any of
- the keywords accepted by :class:`CryptContext`. Direct uses of the
- :class:`!CryptPolicy` constructor should either pass the keywords
- directly into the CryptContext constructor, or to :meth:`CryptContext.update`
- if the policy object was being used to update an existing context object.
-
- In addition to passing in keywords directly,
- CryptPolicy objects can be constructed by the following methods:
-
- .. automethod:: from_path
- .. automethod:: from_string
- .. automethod:: from_source
- .. automethod:: from_sources
- .. automethod:: replace
-
- Introspection
- =============
- All of the informational methods provided by this class have been deprecated
- by identical or similar methods in the :class:`CryptContext` class:
-
- .. automethod:: has_schemes
- .. automethod:: schemes
- .. automethod:: iter_handlers
- .. automethod:: get_handler
- .. automethod:: get_options
- .. automethod:: handler_is_deprecated
- .. automethod:: get_min_verify_time
-
- Exporting
- =========
- .. automethod:: iter_config
- .. automethod:: to_dict
- .. automethod:: to_file
- .. automethod:: to_string
-
- .. note::
- CryptPolicy are immutable.
- Use the :meth:`replace` method to mutate existing instances.
-
- .. deprecated:: 1.6
- """
- #===================================================================
- # class methods
- #===================================================================
- @classmethod
- def from_path(cls, path, section="passlib", encoding="utf-8"):
- """create a CryptPolicy instance from a local file.
-
- .. deprecated:: 1.6
-
- Creating a new CryptContext from a file, which was previously done via
- ``CryptContext(policy=CryptPolicy.from_path(path))``, can now be
- done via ``CryptContext.from_path(path)``.
- See :meth:`CryptContext.from_path` for details.
-
- Updating an existing CryptContext from a file, which was previously done
- ``context.policy = CryptPolicy.from_path(path)``, can now be
- done via ``context.load_path(path)``.
- See :meth:`CryptContext.load_path` for details.
- """
- warn(_preamble +
- "Instead of ``CryptPolicy.from_path(path)``, "
- "use ``CryptContext.from_path(path)`` "
- " or ``context.load_path(path)`` for an existing CryptContext.",
- DeprecationWarning, stacklevel=2)
- return cls(_internal_context=CryptContext.from_path(path, section,
- encoding))
-
- @classmethod
- def from_string(cls, source, section="passlib", encoding="utf-8"):
- """create a CryptPolicy instance from a string.
-
- .. deprecated:: 1.6
-
- Creating a new CryptContext from a string, which was previously done via
- ``CryptContext(policy=CryptPolicy.from_string(data))``, can now be
- done via ``CryptContext.from_string(data)``.
- See :meth:`CryptContext.from_string` for details.
-
- Updating an existing CryptContext from a string, which was previously done
- ``context.policy = CryptPolicy.from_string(data)``, can now be
- done via ``context.load(data)``.
- See :meth:`CryptContext.load` for details.
- """
- warn(_preamble +
- "Instead of ``CryptPolicy.from_string(source)``, "
- "use ``CryptContext.from_string(source)`` or "
- "``context.load(source)`` for an existing CryptContext.",
- DeprecationWarning, stacklevel=2)
- return cls(_internal_context=CryptContext.from_string(source, section,
- encoding))
-
- @classmethod
- def from_source(cls, source, _warn=True):
- """create a CryptPolicy instance from some source.
-
- this method autodetects the source type, and invokes
- the appropriate constructor automatically. it attempts
- to detect whether the source is a configuration string, a filepath,
- a dictionary, or an existing CryptPolicy instance.
-
- .. deprecated:: 1.6
-
- Create a new CryptContext, which could previously be done via
- ``CryptContext(policy=CryptPolicy.from_source(source))``, should
- now be done using an explicit method: the :class:`CryptContext`
- constructor itself, :meth:`CryptContext.from_path`,
- or :meth:`CryptContext.from_string`.
-
- Updating an existing CryptContext, which could previously be done via
- ``context.policy = CryptPolicy.from_source(source)``, should
- now be done using an explicit method: :meth:`CryptContext.update`,
- or :meth:`CryptContext.load`.
- """
- if _warn:
- warn(_preamble +
- "Instead of ``CryptPolicy.from_source()``, "
- "use ``CryptContext.from_string(path)`` "
- " or ``CryptContext.from_path(source)``, as appropriate.",
- DeprecationWarning, stacklevel=2)
- if isinstance(source, CryptPolicy):
- return source
- elif isinstance(source, dict):
- return cls(_internal_context=CryptContext(**source))
- elif not isinstance(source, (bytes,unicode)):
- raise TypeError("source must be CryptPolicy, dict, config string, "
- "or file path: %r" % (type(source),))
- elif any(c in source for c in "\n\r\t") or not source.strip(" \t./\;:"):
- return cls(_internal_context=CryptContext.from_string(source))
- else:
- return cls(_internal_context=CryptContext.from_path(source))
-
- @classmethod
- def from_sources(cls, sources, _warn=True):
- """create a CryptPolicy instance by merging multiple sources.
-
- each source is interpreted as by :meth:`from_source`,
- and the results are merged together.
-
- .. deprecated:: 1.6
- Instead of using this method to merge multiple policies together,
- a :class:`CryptContext` instance should be created, and then
- the multiple sources merged together via :meth:`CryptContext.load`.
- """
- if _warn:
- warn(_preamble +
- "Instead of ``CryptPolicy.from_sources()``, "
- "use the various CryptContext constructors "
- " followed by ``context.update()``.",
- DeprecationWarning, stacklevel=2)
- if len(sources) == 0:
- raise ValueError("no sources specified")
- if len(sources) == 1:
- return cls.from_source(sources[0], _warn=False)
- kwds = {}
- for source in sources:
- kwds.update(cls.from_source(source, _warn=False)._context.to_dict(resolve=True))
- return cls(_internal_context=CryptContext(**kwds))
-
- def replace(self, *args, **kwds):
- """create a new CryptPolicy, optionally updating parts of the
- existing configuration.
-
- .. deprecated:: 1.6
- Callers of this method should :meth:`CryptContext.update` or
- :meth:`CryptContext.copy` instead.
- """
- if self._stub_policy:
- warn(_preamble + # pragma: no cover -- deprecated & unused
- "Instead of ``context.policy.replace()``, "
- "use ``context.update()`` or ``context.copy()``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().replace()``, "
- "create a CryptContext instance and "
- "use ``context.update()`` or ``context.copy()``.",
- DeprecationWarning, stacklevel=2)
- sources = [ self ]
- if args:
- sources.extend(args)
- if kwds:
- sources.append(kwds)
- return CryptPolicy.from_sources(sources, _warn=False)
-
- #===================================================================
- # instance attrs
- #===================================================================
-
- # internal CryptContext we're wrapping to handle everything
- # until this class is removed.
- _context = None
-
- # flag indicating this is wrapper generated by the CryptContext.policy
- # attribute, rather than one created independantly by the application.
- _stub_policy = False
-
- #===================================================================
- # init
- #===================================================================
- def __init__(self, *args, **kwds):
- context = kwds.pop("_internal_context", None)
- if context:
- assert isinstance(context, CryptContext)
- self._context = context
- self._stub_policy = kwds.pop("_stub_policy", False)
- assert not (args or kwds), "unexpected args: %r %r" % (args,kwds)
- else:
- if args:
- if len(args) != 1:
- raise TypeError("only one positional argument accepted")
- if kwds:
- raise TypeError("cannot specify positional arg and kwds")
- kwds = args[0]
- warn(_preamble +
- "Instead of constructing a CryptPolicy instance, "
- "create a CryptContext directly, or use ``context.update()`` "
- "and ``context.load()`` to reconfigure existing CryptContext "
- "instances.",
- DeprecationWarning, stacklevel=2)
- self._context = CryptContext(**kwds)
-
- #===================================================================
- # public interface for examining options
- #===================================================================
- def has_schemes(self):
- """return True if policy defines *any* schemes for use.
-
- .. deprecated:: 1.6
- applications should use ``bool(context.schemes())`` instead.
- see :meth:`CryptContext.schemes`.
- """
- if self._stub_policy:
- warn(_preamble + # pragma: no cover -- deprecated & unused
- "Instead of ``context.policy.has_schemes()``, "
- "use ``bool(context.schemes())``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().has_schemes()``, "
- "create a CryptContext instance and "
- "use ``bool(context.schemes())``.",
- DeprecationWarning, stacklevel=2)
- return bool(self._context.schemes())
-
- def iter_handlers(self):
- """return iterator over handlers defined in policy.
-
- .. deprecated:: 1.6
- applications should use ``context.schemes(resolve=True))`` instead.
- see :meth:`CryptContext.schemes`.
- """
- if self._stub_policy:
- warn(_preamble +
- "Instead of ``context.policy.iter_handlers()``, "
- "use ``context.schemes(resolve=True)``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().iter_handlers()``, "
- "create a CryptContext instance and "
- "use ``context.schemes(resolve=True)``.",
- DeprecationWarning, stacklevel=2)
- return self._context.schemes(resolve=True, unconfigured=True)
-
- def schemes(self, resolve=False):
- """return list of schemes defined in policy.
-
- .. deprecated:: 1.6
- applications should use :meth:`CryptContext.schemes` instead.
- """
- if self._stub_policy:
- warn(_preamble + # pragma: no cover -- deprecated & unused
- "Instead of ``context.policy.schemes()``, "
- "use ``context.schemes()``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().schemes()``, "
- "create a CryptContext instance and "
- "use ``context.schemes()``.",
- DeprecationWarning, stacklevel=2)
- return list(self._context.schemes(resolve=resolve, unconfigured=True))
-
- def get_handler(self, name=None, category=None, required=False):
- """return handler as specified by name, or default handler.
-
- .. deprecated:: 1.6
- applications should use :meth:`CryptContext.handler` instead,
- though note that the ``required`` keyword has been removed,
- and the new method will always act as if ``required=True``.
- """
- if self._stub_policy:
- warn(_preamble +
- "Instead of ``context.policy.get_handler()``, "
- "use ``context.handler()``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().get_handler()``, "
- "create a CryptContext instance and "
- "use ``context.handler()``.",
- DeprecationWarning, stacklevel=2)
- # CryptContext.handler() doesn't support required=False,
- # so wrapping it in try/except
- try:
- return self._context.handler(name, category, unconfigured=True)
- except KeyError:
- if required:
- raise
- else:
- return None
-
- def get_min_verify_time(self, category=None):
- """get min_verify_time setting for policy.
-
- .. deprecated:: 1.6
- min_verify_time option will be removed entirely in passlib 1.8
-
- .. versionchanged:: 1.7
- this method now always returns the value automatically
- calculated by :meth:`CryptContext.min_verify_time`,
- any value specified by policy is ignored.
- """
- warn("get_min_verify_time() and min_verify_time option is deprecated and ignored, "
- "and will be removed in Passlib 1.8", DeprecationWarning,
- stacklevel=2)
- return 0
-
- def get_options(self, name, category=None):
- """return dictionary of options specific to a given handler.
-
- .. deprecated:: 1.6
- this method has no direct replacement in the 1.6 api, as there
- is not a clearly defined use-case. however, examining the output of
- :meth:`CryptContext.to_dict` should serve as the closest alternative.
- """
- # XXX: might make a public replacement, but need more study of the use cases.
- if self._stub_policy:
- warn(_preamble + # pragma: no cover -- deprecated & unused
- "``context.policy.get_options()`` will no longer be available.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "``CryptPolicy().get_options()`` will no longer be available.",
- DeprecationWarning, stacklevel=2)
- if hasattr(name, "name"):
- name = name.name
- return self._context._config._get_record_options_with_flag(name, category)[0]
-
- def handler_is_deprecated(self, name, category=None):
- """check if handler has been deprecated by policy.
-
- .. deprecated:: 1.6
- this method has no direct replacement in the 1.6 api, as there
- is not a clearly defined use-case. however, examining the output of
- :meth:`CryptContext.to_dict` should serve as the closest alternative.
- """
- # XXX: might make a public replacement, but need more study of the use cases.
- if self._stub_policy:
- warn(_preamble +
- "``context.policy.handler_is_deprecated()`` will no longer be available.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "``CryptPolicy().handler_is_deprecated()`` will no longer be available.",
- DeprecationWarning, stacklevel=2)
- if hasattr(name, "name"):
- name = name.name
- return self._context.handler(name, category).deprecated
-
- #===================================================================
- # serialization
- #===================================================================
-
- def iter_config(self, ini=False, resolve=False):
- """iterate over key/value pairs representing the policy object.
-
- .. deprecated:: 1.6
- applications should use :meth:`CryptContext.to_dict` instead.
- """
- if self._stub_policy:
- warn(_preamble + # pragma: no cover -- deprecated & unused
- "Instead of ``context.policy.iter_config()``, "
- "use ``context.to_dict().items()``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().iter_config()``, "
- "create a CryptContext instance and "
- "use ``context.to_dict().items()``.",
- DeprecationWarning, stacklevel=2)
- # hacked code that renders keys & values in manner that approximates
- # old behavior. context.to_dict() is much cleaner.
- context = self._context
- if ini:
- def render_key(key):
- return context._render_config_key(key).replace("__", ".")
- def render_value(value):
- if isinstance(value, (list,tuple)):
- value = ", ".join(value)
- return value
- resolve = False
- else:
- render_key = context._render_config_key
- render_value = lambda value: value
- return (
- (render_key(key), render_value(value))
- for key, value in context._config.iter_config(resolve)
- )
-
- def to_dict(self, resolve=False):
- """export policy object as dictionary of options.
-
- .. deprecated:: 1.6
- applications should use :meth:`CryptContext.to_dict` instead.
- """
- if self._stub_policy:
- warn(_preamble +
- "Instead of ``context.policy.to_dict()``, "
- "use ``context.to_dict()``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().to_dict()``, "
- "create a CryptContext instance and "
- "use ``context.to_dict()``.",
- DeprecationWarning, stacklevel=2)
- return self._context.to_dict(resolve)
-
- def to_file(self, stream, section="passlib"): # pragma: no cover -- deprecated & unused
- """export policy to file.
-
- .. deprecated:: 1.6
- applications should use :meth:`CryptContext.to_string` instead,
- and then write the output to a file as desired.
- """
- if self._stub_policy:
- warn(_preamble +
- "Instead of ``context.policy.to_file(stream)``, "
- "use ``stream.write(context.to_string())``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().to_file(stream)``, "
- "create a CryptContext instance and "
- "use ``stream.write(context.to_string())``.",
- DeprecationWarning, stacklevel=2)
- out = self._context.to_string(section=section)
- if PY2:
- out = out.encode("utf-8")
- stream.write(out)
-
- def to_string(self, section="passlib", encoding=None):
- """export policy to file.
-
- .. deprecated:: 1.6
- applications should use :meth:`CryptContext.to_string` instead.
- """
- if self._stub_policy:
- warn(_preamble + # pragma: no cover -- deprecated & unused
- "Instead of ``context.policy.to_string()``, "
- "use ``context.to_string()``.",
- DeprecationWarning, stacklevel=2)
- else:
- warn(_preamble +
- "Instead of ``CryptPolicy().to_string()``, "
- "create a CryptContext instance and "
- "use ``context.to_string()``.",
- DeprecationWarning, stacklevel=2)
- out = self._context.to_string(section=section)
- if encoding:
- out = out.encode(encoding)
- return out
-
- #===================================================================
- # eoc
- #===================================================================
-
- #=============================================================================
- # _CryptConfig helper class
- #=============================================================================
- class _CryptConfig(object):
- """parses, validates, and stores CryptContext config
-
- this is a helper used internally by CryptContext to handle
- parsing, validation, and serialization of its config options.
- split out from the main class, but not made public since
- that just complicates interface too much (c.f. CryptPolicy)
-
- :arg source: config as dict mapping ``(cat,scheme,option) -> value``
- """
- #===================================================================
- # instance attrs
- #===================================================================
-
- # triple-nested dict which maps scheme -> category -> key -> value,
- # storing all hash-specific options
- _scheme_options = None
-
- # double-nested dict which maps key -> category -> value
- # storing all CryptContext options
- _context_options = None
-
- # tuple of handler objects
- handlers = None
-
- # tuple of scheme objects in same order as handlers
- schemes = None
-
- # tuple of categories in alphabetical order (not including None)
- categories = None
-
- # set of all context keywords used by active schemes
- context_kwds = None
-
- # dict mapping category -> default scheme
- _default_schemes = None
-
- # dict mapping (scheme, category) -> custom handler
- _records = None
-
- # dict mapping category -> list of custom handler instances for that category,
- # in order of schemes(). populated on demand by _get_record_list()
- _record_lists = None
-
- #===================================================================
- # constructor
- #===================================================================
- def __init__(self, source):
- self._init_scheme_list(source.get((None,None,"schemes")))
- self._init_options(source)
- self._init_default_schemes()
- self._init_records()
-
- def _init_scheme_list(self, data):
- """initialize .handlers and .schemes attributes"""
- handlers = []
- schemes = []
- if isinstance(data, native_string_types):
- data = splitcomma(data)
- for elem in data or ():
- # resolve elem -> handler & scheme
- if hasattr(elem, "name"):
- handler = elem
- scheme = handler.name
- _validate_handler_name(scheme)
- elif isinstance(elem, native_string_types):
- handler = get_crypt_handler(elem)
- scheme = handler.name
- else:
- raise TypeError("scheme must be name or CryptHandler, "
- "not %r" % type(elem))
-
- # check scheme name isn't already in use
- if scheme in schemes:
- raise KeyError("multiple handlers with same name: %r" %
- (scheme,))
-
- # add to handler list
- handlers.append(handler)
- schemes.append(scheme)
-
- self.handlers = tuple(handlers)
- self.schemes = tuple(schemes)
-
- #===================================================================
- # lowlevel options
- #===================================================================
-
- #---------------------------------------------------------------
- # init lowlevel option storage
- #---------------------------------------------------------------
- def _init_options(self, source):
- """load config dict into internal representation,
- and init .categories attr
- """
- # prepare dicts & locals
- norm_scheme_option = self._norm_scheme_option
- norm_context_option = self._norm_context_option
- self._scheme_options = scheme_options = {}
- self._context_options = context_options = {}
- categories = set()
-
- # load source config into internal storage
- for (cat, scheme, key), value in iteritems(source):
- categories.add(cat)
- explicit_scheme = scheme
- if not cat and not scheme and key in _global_settings:
- # going forward, not using "<cat>__all__<key>" format. instead...
- # whitelisting set of keys which should be passed to (all) schemes,
- # rather than passed to the CryptContext itself
- scheme = "all"
- if scheme:
- # normalize scheme option
- key, value = norm_scheme_option(key, value)
-
- # e.g. things like "min_rounds" should never be set cross-scheme
- # this will be fatal under 2.0.
- if scheme == "all" and key not in _global_settings:
- warn("The '%s' option should be configured per-algorithm, and not set "
- "globally in the context; This will be an error in Passlib 2.0" %
- (key,), PasslibConfigWarning)
-
- # this scheme is going away in 2.0;
- # but most keys deserve an extra warning since it impacts security.
- if explicit_scheme == "all":
- warn("The 'all' scheme is deprecated as of Passlib 1.7, "
- "and will be removed in Passlib 2.0; Please configure "
- "options on a per-algorithm basis.", DeprecationWarning)
-
- # store in scheme_options
- # map structure: scheme_options[scheme][category][key] = value
- try:
- category_map = scheme_options[scheme]
- except KeyError:
- scheme_options[scheme] = {cat: {key: value}}
- else:
- try:
- option_map = category_map[cat]
- except KeyError:
- category_map[cat] = {key: value}
- else:
- option_map[key] = value
- else:
- # normalize context option
- if cat and key == "schemes":
- raise KeyError("'schemes' context option is not allowed "
- "per category")
- key, value = norm_context_option(cat, key, value)
- if key == "min_verify_time": # ignored in 1.7, to be removed in 1.8
- continue
-
- # store in context_options
- # map structure: context_options[key][category] = value
- try:
- category_map = context_options[key]
- except KeyError:
- context_options[key] = {cat: value}
- else:
- category_map[cat] = value
-
- # store list of configured categories
- categories.discard(None)
- self.categories = tuple(sorted(categories))
-
- def _norm_scheme_option(self, key, value):
- # check for invalid options
- if key in _forbidden_scheme_options:
- raise KeyError("%r option not allowed in CryptContext "
- "configuration" % (key,))
- # coerce strings for certain fields (e.g. min_rounds uses ints)
- if isinstance(value, native_string_types):
- func = _coerce_scheme_options.get(key)
- if func:
- value = func(value)
- return key, value
-
- def _norm_context_option(self, cat, key, value):
- schemes = self.schemes
- if key == "default":
- if hasattr(value, "name"):
- value = value.name
- elif not isinstance(value, native_string_types):
- raise ExpectedTypeError(value, "str", "default")
- if schemes and value not in schemes:
- raise KeyError("default scheme not found in policy")
- elif key == "deprecated":
- if isinstance(value, native_string_types):
- value = splitcomma(value)
- elif not isinstance(value, (list,tuple)):
- raise ExpectedTypeError(value, "str or seq", "deprecated")
- if 'auto' in value:
- # XXX: have any statements been made about when this is default?
- # should do it in 1.8 at latest.
- if len(value) > 1:
- raise ValueError("cannot list other schemes if "
- "``deprecated=['auto']`` is used")
- elif schemes:
- # make sure list of deprecated schemes is subset of configured schemes
- for scheme in value:
- if not isinstance(scheme, native_string_types):
- raise ExpectedTypeError(value, "str", "deprecated element")
- if scheme not in schemes:
- raise KeyError("deprecated scheme not found "
- "in policy: %r" % (scheme,))
- elif key == "min_verify_time":
- warn("'min_verify_time' was deprecated in Passlib 1.6, is "
- "ignored in 1.7, and will be removed in 1.8",
- DeprecationWarning)
- elif key == "harden_verify":
- warn("'harden_verify' is deprecated & ignored as of Passlib 1.7.1, "
- " and will be removed in 1.8",
- DeprecationWarning)
- elif key != "schemes":
- raise KeyError("unknown CryptContext keyword: %r" % (key,))
- return key, value
-
- #---------------------------------------------------------------
- # reading context options
- #---------------------------------------------------------------
- def get_context_optionmap(self, key, _default={}):
- """return dict mapping category->value for specific context option.
-
- .. warning:: treat return value as readonly!
- """
- return self._context_options.get(key, _default)
-
- def get_context_option_with_flag(self, category, key):
- """return value of specific option, handling category inheritance.
- also returns flag indicating whether value is category-specific.
- """
- try:
- category_map = self._context_options[key]
- except KeyError:
- return None, False
- value = category_map.get(None)
- if category:
- try:
- alt = category_map[category]
- except KeyError:
- pass
- else:
- if value is None or alt != value:
- return alt, True
- return value, False
-
- #---------------------------------------------------------------
- # reading scheme options
- #---------------------------------------------------------------
- def _get_scheme_optionmap(self, scheme, category, default={}):
- """return all options for (scheme,category) combination
-
- .. warning:: treat return value as readonly!
- """
- try:
- return self._scheme_options[scheme][category]
- except KeyError:
- return default
-
- def get_base_handler(self, scheme):
- return self.handlers[self.schemes.index(scheme)]
-
- @staticmethod
- def expand_settings(handler):
- setting_kwds = handler.setting_kwds
- if 'rounds' in handler.setting_kwds:
- # XXX: historically this extras won't be listed in setting_kwds
- setting_kwds += uh.HasRounds.using_rounds_kwds
- return setting_kwds
-
- # NOTE: this is only used by _get_record_options_with_flag()...
- def get_scheme_options_with_flag(self, scheme, category):
- """return composite dict of all options set for scheme.
- includes options inherited from 'all' and from default category.
- result can be modified.
- returns (kwds, has_cat_specific_options)
- """
- # start out with copy of global options
- get_optionmap = self._get_scheme_optionmap
- kwds = get_optionmap("all", None).copy()
- has_cat_options = False
-
- # add in category-specific global options
- if category:
- defkwds = kwds.copy() # <-- used to detect category-specific options
- kwds.update(get_optionmap("all", category))
-
- # filter out global settings not supported by handler
- allowed_settings = self.expand_settings(self.get_base_handler(scheme))
- for key in set(kwds).difference(allowed_settings):
- kwds.pop(key)
- if category:
- for key in set(defkwds).difference(allowed_settings):
- defkwds.pop(key)
-
- # add in default options for scheme
- other = get_optionmap(scheme, None)
- kwds.update(other)
-
- # load category-specific options for scheme
- if category:
- defkwds.update(other)
- kwds.update(get_optionmap(scheme, category))
-
- # compare default category options to see if there's anything
- # category-specific
- if kwds != defkwds:
- has_cat_options = True
-
- return kwds, has_cat_options
-
- #===================================================================
- # deprecated & default schemes
- #===================================================================
- def _init_default_schemes(self):
- """initialize maps containing default scheme for each category.
-
- have to do this after _init_options(), since the default scheme
- is affected by the list of deprecated schemes.
- """
- # init maps & locals
- get_optionmap = self.get_context_optionmap
- default_map = self._default_schemes = get_optionmap("default").copy()
- dep_map = get_optionmap("deprecated")
- schemes = self.schemes
- if not schemes:
- return
-
- # figure out default scheme
- deps = dep_map.get(None) or ()
- default = default_map.get(None)
- if not default:
- for scheme in schemes:
- if scheme not in deps:
- default_map[None] = scheme
- break
- else:
- raise ValueError("must have at least one non-deprecated scheme")
- elif default in deps:
- raise ValueError("default scheme cannot be deprecated")
-
- # figure out per-category default schemes,
- for cat in self.categories:
- cdeps = dep_map.get(cat, deps)
- cdefault = default_map.get(cat, default)
- if not cdefault:
- for scheme in schemes:
- if scheme not in cdeps:
- default_map[cat] = scheme
- break
- else:
- raise ValueError("must have at least one non-deprecated "
- "scheme for %r category" % cat)
- elif cdefault in cdeps:
- raise ValueError("default scheme for %r category "
- "cannot be deprecated" % cat)
-
- def default_scheme(self, category):
- """return default scheme for specific category"""
- defaults = self._default_schemes
- try:
- return defaults[category]
- except KeyError:
- pass
- if not self.schemes:
- raise KeyError("no hash schemes configured for this "
- "CryptContext instance")
- return defaults[None]
-
- def is_deprecated_with_flag(self, scheme, category):
- """is scheme deprecated under particular category?"""
- depmap = self.get_context_optionmap("deprecated")
- def test(cat):
- source = depmap.get(cat, depmap.get(None))
- if source is None:
- return None
- elif 'auto' in source:
- return scheme != self.default_scheme(cat)
- else:
- return scheme in source
- value = test(None) or False
- if category:
- alt = test(category)
- if alt is not None and value != alt:
- return alt, True
- return value, False
-
- #===================================================================
- # CryptRecord objects
- #===================================================================
- def _init_records(self):
- # NOTE: this step handles final validation of settings,
- # checking for violations against handler's internal invariants.
- # this is why we create all the records now,
- # so CryptContext throws error immediately rather than later.
- self._record_lists = {}
- records = self._records = {}
- all_context_kwds = self.context_kwds = set()
- get_options = self._get_record_options_with_flag
- categories = (None,) + self.categories
- for handler in self.handlers:
- scheme = handler.name
- all_context_kwds.update(handler.context_kwds)
- for cat in categories:
- kwds, has_cat_options = get_options(scheme, cat)
- if cat is None or has_cat_options:
- records[scheme, cat] = self._create_record(handler, cat, **kwds)
- # NOTE: if handler has no category-specific opts, get_record()
- # will automatically use the default category's record.
- # NOTE: default records for specific category stored under the
- # key (None,category); these are populated on-demand by get_record().
-
- @staticmethod
- def _create_record(handler, category=None, deprecated=False, **settings):
- # create custom handler if needed.
- try:
- # XXX: relaxed=True is mostly here to retain backwards-compat behavior.
- # could make this optional flag in future.
- subcls = handler.using(relaxed=True, **settings)
- except TypeError as err:
- m = re.match(r".* unexpected keyword argument '(.*)'$", str(err))
- if m and m.group(1) in settings:
- # translate into KeyError, for backwards compat.
- # XXX: push this down to GenericHandler.using() implementation?
- key = m.group(1)
- raise KeyError("keyword not supported by %s handler: %r" %
- (handler.name, key))
- raise
-
- # using private attrs to store some extra metadata in custom handler
- assert subcls is not handler, "expected unique variant of handler"
- ##subcls._Context__category = category
- subcls._Context__orig_handler = handler
- subcls.deprecated = deprecated # attr reserved for this purpose
- return subcls
-
- def _get_record_options_with_flag(self, scheme, category):
- """return composite dict of options for given scheme + category.
-
- this is currently a private method, though some variant
- of its output may eventually be made public.
-
- given a scheme & category, it returns two things:
- a set of all the keyword options to pass to :meth:`_create_record`,
- and a bool flag indicating whether any of these options
- were specific to the named category. if this flag is false,
- the options are identical to the options for the default category.
-
- the options dict includes all the scheme-specific settings,
- as well as optional *deprecated* keyword.
- """
- # get scheme options
- kwds, has_cat_options = self.get_scheme_options_with_flag(scheme, category)
-
- # throw in deprecated flag
- value, not_inherited = self.is_deprecated_with_flag(scheme, category)
- if value:
- kwds['deprecated'] = True
- if not_inherited:
- has_cat_options = True
-
- return kwds, has_cat_options
-
- def get_record(self, scheme, category):
- """return record for specific scheme & category (cached)"""
- # NOTE: this is part of the critical path shared by
- # all of CryptContext's PasswordHash methods,
- # hence all the caching and error checking.
-
- # quick lookup in cache
- try:
- return self._records[scheme, category]
- except KeyError:
- pass
-
- # type check
- if category is not None and not isinstance(category, native_string_types):
- if PY2 and isinstance(category, unicode):
- # for compatibility with unicode-centric py2 apps
- return self.get_record(scheme, category.encode("utf-8"))
- raise ExpectedTypeError(category, "str or None", "category")
- if scheme is not None and not isinstance(scheme, native_string_types):
- raise ExpectedTypeError(scheme, "str or None", "scheme")
-
- # if scheme=None,
- # use record for category's default scheme, and cache result.
- if not scheme:
- default = self.default_scheme(category)
- assert default
- record = self._records[None, category] = self.get_record(default,
- category)
- return record
-
- # if no record for (scheme, category),
- # use record for (scheme, None), and cache result.
- if category:
- try:
- cache = self._records
- record = cache[scheme, category] = cache[scheme, None]
- return record
- except KeyError:
- pass
-
- # scheme not found in configuration for default category
- raise KeyError("crypt algorithm not found in policy: %r" % (scheme,))
-
- def _get_record_list(self, category=None):
- """return list of records for category (cached)
-
- this is an internal helper used only by identify_record()
- """
- # type check of category - handled by _get_record()
- # quick lookup in cache
- try:
- return self._record_lists[category]
- except KeyError:
- pass
- # cache miss - build list from scratch
- value = self._record_lists[category] = [
- self.get_record(scheme, category)
- for scheme in self.schemes
- ]
- return value
-
- def identify_record(self, hash, category, required=True):
- """internal helper to identify appropriate custom handler for hash"""
- # NOTE: this is part of the critical path shared by
- # all of CryptContext's PasswordHash methods,
- # hence all the caching and error checking.
- # FIXME: if multiple hashes could match (e.g. lmhash vs nthash)
- # this will only return first match. might want to do something
- # about this in future, but for now only hashes with
- # unique identifiers will work properly in a CryptContext.
- # XXX: if all handlers have a unique prefix (e.g. all are MCF / LDAP),
- # could use dict-lookup to speed up this search.
- if not isinstance(hash, unicode_or_bytes_types):
- raise ExpectedStringError(hash, "hash")
- # type check of category - handled by _get_record_list()
- for record in self._get_record_list(category):
- if record.identify(hash):
- return record
- if not required:
- return None
- elif not self.schemes:
- raise KeyError("no crypt algorithms supported")
- else:
- raise ValueError("hash could not be identified")
-
- @memoized_property
- def disabled_record(self):
- for record in self._get_record_list(None):
- if record.is_disabled:
- return record
- raise RuntimeError("no disabled hasher present "
- "(perhaps add 'unix_disabled' to list of schemes?)")
-
- #===================================================================
- # serialization
- #===================================================================
- def iter_config(self, resolve=False):
- """regenerate original config.
-
- this is an iterator which yields ``(cat,scheme,option),value`` items,
- in the order they generally appear inside an INI file.
- if interpreted as a dictionary, it should match the original
- keywords passed to the CryptContext (aside from any canonization).
-
- it's mainly used as the internal backend for most of the public
- serialization methods.
- """
- # grab various bits of data
- scheme_options = self._scheme_options
- context_options = self._context_options
- scheme_keys = sorted(scheme_options)
- context_keys = sorted(context_options)
-
- # write loaded schemes (may differ from 'schemes' local var)
- if 'schemes' in context_keys:
- context_keys.remove("schemes")
- value = self.handlers if resolve else self.schemes
- if value:
- yield (None, None, "schemes"), list(value)
-
- # then run through config for each user category
- for cat in (None,) + self.categories:
-
- # write context options
- for key in context_keys:
- try:
- value = context_options[key][cat]
- except KeyError:
- pass
- else:
- if isinstance(value, list):
- value = list(value)
- yield (cat, None, key), value
-
- # write per-scheme options for all schemes.
- for scheme in scheme_keys:
- try:
- kwds = scheme_options[scheme][cat]
- except KeyError:
- pass
- else:
- for key in sorted(kwds):
- yield (cat, scheme, key), kwds[key]
-
- #===================================================================
- # eoc
- #===================================================================
-
- #=============================================================================
- # main CryptContext class
- #=============================================================================
- class CryptContext(object):
- """Helper for hashing & verifying passwords using multiple algorithms.
-
- Instances of this class allow applications to choose a specific
- set of hash algorithms which they wish to support, set limits and defaults
- for the rounds and salt sizes those algorithms should use, flag
- which algorithms should be deprecated, and automatically handle
- migrating users to stronger hashes when they log in.
-
- Basic usage::
-
- >>> ctx = CryptContext(schemes=[...])
-
- See the Passlib online documentation for details and full documentation.
- """
- # FIXME: altering the configuration of this object isn't threadsafe,
- # but is generally only done during application init, so not a major
- # issue (just yet).
-
- # XXX: would like some way to restrict the categories that are allowed,
- # to restrict what the app OR the config can use.
-
- # XXX: add wrap/unwrap callback hooks so app can mutate hash format?
-
- # XXX: add method for detecting and warning user about schemes
- # which don't have any good distinguishing marks?
- # or greedy ones (unix_disabled, plaintext) which are not listed at the end?
-
- #===================================================================
- # instance attrs
- #===================================================================
-
- # _CryptConfig instance holding current parsed config
- _config = None
-
- # copy of _config methods, stored in CryptContext instance for speed.
- _get_record = None
- _identify_record = None
-
- #===================================================================
- # secondary constructors
- #===================================================================
- @classmethod
- def _norm_source(cls, source):
- """internal helper - accepts string, dict, or context"""
- if isinstance(source, dict):
- return cls(**source)
- elif isinstance(source, cls):
- return source
- else:
- self = cls()
- self.load(source)
- return self
-
- @classmethod
- def from_string(cls, source, section="passlib", encoding="utf-8"):
- """create new CryptContext instance from an INI-formatted string.
-
- :type source: unicode or bytes
- :arg source:
- string containing INI-formatted content.
-
- :type section: str
- :param section:
- option name of section to read from, defaults to ``"passlib"``.
-
- :type encoding: str
- :arg encoding:
- optional encoding used when source is bytes, defaults to ``"utf-8"``.
-
- :returns:
- new :class:`CryptContext` instance, configured based on the
- parameters in the *source* string.
-
- Usage example::
-
- >>> from passlib.context import CryptContext
- >>> context = CryptContext.from_string('''
- ... [passlib]
- ... schemes = sha256_crypt, des_crypt
- ... sha256_crypt__default_rounds = 30000
- ... ''')
-
- .. versionadded:: 1.6
-
- .. seealso:: :meth:`to_string`, the inverse of this constructor.
- """
- if not isinstance(source, unicode_or_bytes_types):
- raise ExpectedTypeError(source, "unicode or bytes", "source")
- self = cls(_autoload=False)
- self.load(source, section=section, encoding=encoding)
- return self
-
- @classmethod
- def from_path(cls, path, section="passlib", encoding="utf-8"):
- """create new CryptContext instance from an INI-formatted file.
-
- this functions exactly the same as :meth:`from_string`,
- except that it loads from a local file.
-
- :type path: str
- :arg path:
- path to local file containing INI-formatted config.
-
- :type section: str
- :param section:
- option name of section to read from, defaults to ``"passlib"``.
-
- :type encoding: str
- :arg encoding:
- encoding used to load file, defaults to ``"utf-8"``.
-
- :returns:
- new CryptContext instance, configured based on the parameters
- stored in the file *path*.
-
- .. versionadded:: 1.6
-
- .. seealso:: :meth:`from_string` for an equivalent usage example.
- """
- self = cls(_autoload=False)
- self.load_path(path, section=section, encoding=encoding)
- return self
-
- def copy(self, **kwds):
- """Return copy of existing CryptContext instance.
-
- This function returns a new CryptContext instance whose configuration
- is exactly the same as the original, with the exception that any keywords
- passed in will take precedence over the original settings.
- As an example::
-
- >>> from passlib.context import CryptContext
-
- >>> # given an existing context...
- >>> ctx1 = CryptContext(["sha256_crypt", "md5_crypt"])
-
- >>> # copy can be used to make a clone, and update
- >>> # some of the settings at the same time...
- >>> ctx2 = custom_app_context.copy(default="md5_crypt")
-
- >>> # and the original will be unaffected by the change
- >>> ctx1.default_scheme()
- "sha256_crypt"
- >>> ctx2.default_scheme()
- "md5_crypt"
-
- .. versionadded:: 1.6
- This method was previously named :meth:`!replace`. That alias
- has been deprecated, and will be removed in Passlib 1.8.
-
- .. seealso:: :meth:`update`
- """
- # XXX: it would be faster to store ref to self._config,
- # but don't want to share config objects til sure
- # can rely on them being immutable.
- other = CryptContext(_autoload=False)
- other.load(self)
- if kwds:
- other.load(kwds, update=True)
- return other
-
- def using(self, **kwds):
- """
- alias for :meth:`copy`, to match PasswordHash.using()
- """
- return self.copy(**kwds)
-
- def replace(self, **kwds):
- """deprecated alias of :meth:`copy`"""
- warn("CryptContext().replace() has been deprecated in Passlib 1.6, "
- "and will be removed in Passlib 1.8, "
- "it has been renamed to CryptContext().copy()",
- DeprecationWarning, stacklevel=2)
- return self.copy(**kwds)
-
- #===================================================================
- # init
- #===================================================================
- def __init__(self, schemes=None,
- # keyword only...
- policy=_UNSET, # <-- deprecated
- _autoload=True, **kwds):
- # XXX: add ability to make flag certain contexts as immutable,
- # e.g. the builtin passlib ones?
- # XXX: add a name or import path for the contexts, to help out repr?
- if schemes is not None:
- kwds['schemes'] = schemes
- if policy is not _UNSET:
- warn("The CryptContext ``policy`` keyword has been deprecated as of Passlib 1.6, "
- "and will be removed in Passlib 1.8; please use "
- "``CryptContext.from_string()` or "
- "``CryptContext.from_path()`` instead.",
- DeprecationWarning)
- if policy is None:
- self.load(kwds)
- elif isinstance(policy, CryptPolicy):
- self.load(policy._context)
- self.update(kwds)
- else:
- raise TypeError("policy must be a CryptPolicy instance")
- elif _autoload:
- self.load(kwds)
- else:
- assert not kwds, "_autoload=False and kwds are mutually exclusive"
-
- # XXX: would this be useful?
- ##def __str__(self):
- ## if PY3:
- ## return self.to_string()
- ## else:
- ## return self.to_string().encode("utf-8")
-
- def __repr__(self):
- return "<CryptContext at 0x%0x>" % id(self)
-
- #===================================================================
- # deprecated policy object
- #===================================================================
- def _get_policy(self):
- # The CryptPolicy class has been deprecated, so to support any
- # legacy accesses, we create a stub policy object so .policy attr
- # will continue to work.
- #
- # the code waits until app accesses a specific policy object attribute
- # before issuing deprecation warning, so developer gets method-specific
- # suggestion for how to upgrade.
-
- # NOTE: making a copy of the context so the policy acts like a snapshot,
- # to retain the pre-1.6 behavior.
- return CryptPolicy(_internal_context=self.copy(), _stub_policy=True)
-
- def _set_policy(self, policy):
- warn("The CryptPolicy class and the ``context.policy`` attribute have "
- "been deprecated as of Passlib 1.6, and will be removed in "
- "Passlib 1.8; please use the ``context.load()`` and "
- "``context.update()`` methods instead.",
- DeprecationWarning, stacklevel=2)
- if isinstance(policy, CryptPolicy):
- self.load(policy._context)
- else:
- raise TypeError("expected CryptPolicy instance")
-
- policy = property(_get_policy, _set_policy,
- doc="[deprecated] returns CryptPolicy instance "
- "tied to this CryptContext")
-
- #===================================================================
- # loading / updating configuration
- #===================================================================
- @staticmethod
- def _parse_ini_stream(stream, section, filename):
- """helper read INI from stream, extract passlib section as dict"""
- # NOTE: this expects a unicode stream under py3,
- # and a utf-8 bytes stream under py2,
- # allowing the resulting dict to always use native strings.
- p = SafeConfigParser()
- if PY3:
- # python 3.2 deprecated readfp in favor of read_file
- p.read_file(stream, filename)
- else:
- p.readfp(stream, filename)
- # XXX: could change load() to accept list of items,
- # and skip intermediate dict creation
- return dict(p.items(section))
-
- def load_path(self, path, update=False, section="passlib", encoding="utf-8"):
- """Load new configuration into CryptContext from a local file.
-
- This function is a wrapper for :meth:`load` which
- loads a configuration string from the local file *path*,
- instead of an in-memory source. Its behavior and options
- are otherwise identical to :meth:`!load` when provided with
- an INI-formatted string.
-
- .. versionadded:: 1.6
- """
- def helper(stream):
- kwds = self._parse_ini_stream(stream, section, path)
- return self.load(kwds, update=update)
- if PY3:
- # decode to unicode, which load() expected under py3
- with open(path, "rt", encoding=encoding) as stream:
- return helper(stream)
- elif encoding in ["utf-8", "ascii"]:
- # keep as utf-8 bytes, which load() expects under py2
- with open(path, "rb") as stream:
- return helper(stream)
- else:
- # transcode to utf-8 bytes
- with open(path, "rb") as fh:
- tmp = fh.read().decode(encoding).encode("utf-8")
- return helper(BytesIO(tmp))
-
- def load(self, source, update=False, section="passlib", encoding="utf-8"):
- """Load new configuration into CryptContext, replacing existing config.
-
- :arg source:
- source of new configuration to load.
- this value can be a number of different types:
-
- * a :class:`!dict` object, or compatible Mapping
-
- the key/value pairs will be interpreted the same
- keywords for the :class:`CryptContext` class constructor.
-
- * a :class:`!unicode` or :class:`!bytes` string
-
- this will be interpreted as an INI-formatted file,
- and appropriate key/value pairs will be loaded from
- the specified *section*.
-
- * another :class:`!CryptContext` object.
-
- this will export a snapshot of its configuration
- using :meth:`to_dict`.
-
- :type update: bool
- :param update:
- By default, :meth:`load` will replace the existing configuration
- entirely. If ``update=True``, it will preserve any existing
- configuration options that are not overridden by the new source,
- much like the :meth:`update` method.
-
- :type section: str
- :param section:
- When parsing an INI-formatted string, :meth:`load` will look for
- a section named ``"passlib"``. This option allows an alternate
- section name to be used. Ignored when loading from a dictionary.
-
- :type encoding: str
- :param encoding:
- Encoding to use when decode bytes from string.
- Defaults to ``"utf-8"``. Ignoring when loading from a dictionary.
-
- :raises TypeError:
- * If the source cannot be identified.
- * If an unknown / malformed keyword is encountered.
-
- :raises ValueError:
- If an invalid keyword value is encountered.
-
- .. note::
-
- If an error occurs during a :meth:`!load` call, the :class:`!CryptContext`
- instance will be restored to the configuration it was in before
- the :meth:`!load` call was made; this is to ensure it is
- *never* left in an inconsistent state due to a load error.
-
- .. versionadded:: 1.6
- """
- #-----------------------------------------------------------
- # autodetect source type, convert to dict
- #-----------------------------------------------------------
- parse_keys = True
- if isinstance(source, unicode_or_bytes_types):
- if PY3:
- source = to_unicode(source, encoding, param="source")
- else:
- source = to_bytes(source, "utf-8", source_encoding=encoding,
- param="source")
- source = self._parse_ini_stream(NativeStringIO(source), section,
- "<string passed to CryptContext.load()>")
- elif isinstance(source, CryptContext):
- # extract dict directly from config, so it can be merged later
- source = dict(source._config.iter_config(resolve=True))
- parse_keys = False
- elif not hasattr(source, "items"):
- # mappings are left alone, otherwise throw an error.
- raise ExpectedTypeError(source, "string or dict", "source")
-
- # XXX: add support for other iterable types, e.g. sequence of pairs?
-
- #-----------------------------------------------------------
- # parse dict keys into (category, scheme, option) format,
- # and merge with existing configuration if needed.
- #-----------------------------------------------------------
- if parse_keys:
- parse = self._parse_config_key
- source = dict((parse(key), value)
- for key, value in iteritems(source))
- if update and self._config is not None:
- # if updating, do nothing if source is empty,
- if not source:
- return
- # otherwise overlay source on top of existing config
- tmp = source
- source = dict(self._config.iter_config(resolve=True))
- source.update(tmp)
-
- #-----------------------------------------------------------
- # compile into _CryptConfig instance, and update state
- #-----------------------------------------------------------
- config = _CryptConfig(source)
- self._config = config
- self._reset_dummy_verify()
- self._get_record = config.get_record
- self._identify_record = config.identify_record
- if config.context_kwds:
- # (re-)enable method for this instance (in case ELSE clause below ran last load).
- self.__dict__.pop("_strip_unused_context_kwds", None)
- else:
- # disable method for this instance, it's not needed.
- self._strip_unused_context_kwds = None
-
- @staticmethod
- def _parse_config_key(ckey):
- """helper used to parse ``cat__scheme__option`` keys into a tuple"""
- # split string into 1-3 parts
- assert isinstance(ckey, native_string_types)
- parts = ckey.replace(".", "__").split("__")
- count = len(parts)
- if count == 1:
- cat, scheme, key = None, None, parts[0]
- elif count == 2:
- cat = None
- scheme, key = parts
- elif count == 3:
- cat, scheme, key = parts
- else:
- raise TypeError("keys must have less than 3 separators: %r" %
- (ckey,))
- # validate & normalize the parts
- if cat == "default":
- cat = None
- elif not cat and cat is not None:
- raise TypeError("empty category: %r" % ckey)
- if scheme == "context":
- scheme = None
- elif not scheme and scheme is not None:
- raise TypeError("empty scheme: %r" % ckey)
- if not key:
- raise TypeError("empty option: %r" % ckey)
- return cat, scheme, key
-
- def update(self, *args, **kwds):
- """Helper for quickly changing configuration.
-
- This acts much like the :meth:`!dict.update` method:
- it updates the context's configuration,
- replacing the original value(s) for the specified keys,
- and preserving the rest.
- It accepts any :ref:`keyword <context-options>`
- accepted by the :class:`!CryptContext` constructor.
-
- .. versionadded:: 1.6
-
- .. seealso:: :meth:`copy`
- """
- if args:
- if len(args) > 1:
- raise TypeError("expected at most one positional argument")
- if kwds:
- raise TypeError("positional arg and keywords mutually exclusive")
- self.load(args[0], update=True)
- elif kwds:
- self.load(kwds, update=True)
-
- # XXX: make this public? even just as flag to load?
- # FIXME: this function suffered some bitrot in 1.6.1,
- # will need to be updated before works again.
- ##def _simplify(self):
- ## "helper to remove redundant/unused options"
- ## # don't do anything if no schemes are defined
- ## if not self._schemes:
- ## return
- ##
- ## def strip_items(target, filter):
- ## keys = [key for key,value in iteritems(target)
- ## if filter(key,value)]
- ## for key in keys:
- ## del target[key]
- ##
- ## # remove redundant default.
- ## defaults = self._default_schemes
- ## if defaults.get(None) == self._schemes[0]:
- ## del defaults[None]
- ##
- ## # remove options for unused schemes.
- ## scheme_options = self._scheme_options
- ## schemes = self._schemes + ("all",)
- ## strip_items(scheme_options, lambda k,v: k not in schemes)
- ##
- ## # remove rendundant cat defaults.
- ## cur = self.default_scheme()
- ## strip_items(defaults, lambda k,v: k and v==cur)
- ##
- ## # remove redundant category deprecations.
- ## # TODO: this should work w/ 'auto', but needs closer inspection
- ## deprecated = self._deprecated_schemes
- ## cur = self._deprecated_schemes.get(None)
- ## strip_items(deprecated, lambda k,v: k and v==cur)
- ##
- ## # remove redundant category options.
- ## for scheme, config in iteritems(scheme_options):
- ## if None in config:
- ## cur = config[None]
- ## strip_items(config, lambda k,v: k and v==cur)
- ##
- ## # XXX: anything else?
-
- #===================================================================
- # reading configuration
- #===================================================================
- def schemes(self, resolve=False, category=None, unconfigured=False):
- """return schemes loaded into this CryptContext instance.
-
- :type resolve: bool
- :arg resolve:
- if ``True``, will return a tuple of :class:`~passlib.ifc.PasswordHash`
- objects instead of their names.
-
- :returns:
- returns tuple of the schemes configured for this context
- via the *schemes* option.
-
- .. versionadded:: 1.6
- This was previously available as ``CryptContext().policy.schemes()``
-
- .. seealso:: the :ref:`schemes <context-schemes-option>` option for usage example.
- """
- # XXX: should resolv return records rather than handlers?
- # or deprecate resolve keyword completely?
- # offering up a .hashers Mapping in v1.8 would be great.
- # NOTE: supporting 'category' and 'unconfigured' kwds as of 1.7
- # just to pass through to .handler(), but not documenting them...
- # may not need to put them to use.
- schemes = self._config.schemes
- if resolve:
- return tuple(self.handler(scheme, category, unconfigured=unconfigured)
- for scheme in schemes)
- else:
- return schemes
-
- def default_scheme(self, category=None, resolve=False, unconfigured=False):
- """return name of scheme that :meth:`hash` will use by default.
-
- :type resolve: bool
- :arg resolve:
- if ``True``, will return a :class:`~passlib.ifc.PasswordHash`
- object instead of the name.
-
- :type category: str or None
- :param category:
- Optional :ref:`user category <user-categories>`.
- If specified, this will return the catgory-specific default scheme instead.
-
- :returns:
- name of the default scheme.
-
- .. seealso:: the :ref:`default <context-default-option>` option for usage example.
-
- .. versionadded:: 1.6
-
- .. versionchanged:: 1.7
-
- This now returns a hasher configured with any CryptContext-specific
- options (custom rounds settings, etc). Previously this returned
- the base hasher from :mod:`passlib.hash`.
- """
- # XXX: deprecate this in favor of .handler() or whatever it's replaced with?
- # NOTE: supporting 'unconfigured' kwds as of 1.7
- # just to pass through to .handler(), but not documenting them...
- # may not need to put them to use.
- hasher = self.handler(None, category, unconfigured=unconfigured)
- return hasher if resolve else hasher.name
-
- # XXX: need to decide if exposing this would be useful in any way
- ##def categories(self):
- ## """return user-categories with algorithm-specific options in this CryptContext.
- ##
- ## this will always return a tuple.
- ## if no categories besides the default category have been configured,
- ## the tuple will be empty.
- ## """
- ## return self._config.categories
-
- # XXX: need to decide if exposing this would be useful to applications
- # in any meaningful way that isn't already served by to_dict()
- ##def options(self, scheme, category=None):
- ## kwds, percat = self._config.get_options(scheme, category)
- ## return kwds
-
- def handler(self, scheme=None, category=None, unconfigured=False):
- """helper to resolve name of scheme -> :class:`~passlib.ifc.PasswordHash` object used by scheme.
-
- :arg scheme:
- This should identify the scheme to lookup.
- If omitted or set to ``None``, this will return the handler
- for the default scheme.
-
- :arg category:
- If a user category is specified, and no scheme is provided,
- it will use the default for that category.
- Otherwise this parameter is ignored.
-
- :param unconfigured:
-
- By default, this returns a handler object whose .hash()
- and .needs_update() methods will honor the configured
- provided by CryptContext. See ``unconfigured=True``
- to get the underlying handler from before any context-specific
- configuration was applied.
-
- :raises KeyError:
- If the scheme does not exist OR is not being used within this context.
-
- :returns:
- :class:`~passlib.ifc.PasswordHash` object used to implement
- the named scheme within this context (this will usually
- be one of the objects from :mod:`passlib.hash`)
-
- .. versionadded:: 1.6
- This was previously available as ``CryptContext().policy.get_handler()``
-
- .. versionchanged:: 1.7
-
- This now returns a hasher configured with any CryptContext-specific
- options (custom rounds settings, etc). Previously this returned
- the base hasher from :mod:`passlib.hash`.
- """
- try:
- hasher = self._get_record(scheme, category)
- if unconfigured:
- return hasher._Context__orig_handler
- else:
- return hasher
- except KeyError:
- pass
- if self._config.handlers:
- raise KeyError("crypt algorithm not found in this "
- "CryptContext instance: %r" % (scheme,))
- else:
- raise KeyError("no crypt algorithms loaded in this "
- "CryptContext instance")
-
- def _get_unregistered_handlers(self):
- """check if any handlers in this context aren't in the global registry"""
- return tuple(handler for handler in self._config.handlers
- if not _is_handler_registered(handler))
-
- @property
- def context_kwds(self):
- """
- return :class:`!set` containing union of all :ref:`contextual keywords <context-keywords>`
- supported by the handlers in this context.
-
- .. versionadded:: 1.6.6
- """
- return self._config.context_kwds
-
- #===================================================================
- # exporting config
- #===================================================================
- @staticmethod
- def _render_config_key(key):
- """convert 3-part config key to single string"""
- cat, scheme, option = key
- if cat:
- return "%s__%s__%s" % (cat, scheme or "context", option)
- elif scheme:
- return "%s__%s" % (scheme, option)
- else:
- return option
-
- @staticmethod
- def _render_ini_value(key, value):
- """render value to string suitable for INI file"""
- # convert lists to comma separated lists
- # (mainly 'schemes' & 'deprecated')
- if isinstance(value, (list,tuple)):
- value = ", ".join(value)
-
- # convert numbers to strings
- elif isinstance(value, num_types):
- if isinstance(value, float) and key[2] == "vary_rounds":
- value = ("%.2f" % value).rstrip("0") if value else "0"
- else:
- value = str(value)
-
- assert isinstance(value, native_string_types), \
- "expected string for key: %r %r" % (key, value)
-
- # escape any percent signs.
- return value.replace("%", "%%")
-
- def to_dict(self, resolve=False):
- """Return current configuration as a dictionary.
-
- :type resolve: bool
- :arg resolve:
- if ``True``, the ``schemes`` key will contain a list of
- a :class:`~passlib.ifc.PasswordHash` objects instead of just
- their names.
-
- This method dumps the current configuration of the CryptContext
- instance. The key/value pairs should be in the format accepted
- by the :class:`!CryptContext` class constructor, in fact
- ``CryptContext(**myctx.to_dict())`` will create an exact copy of ``myctx``.
- As an example::
-
- >>> # you can dump the configuration of any crypt context...
- >>> from passlib.apps import ldap_nocrypt_context
- >>> ldap_nocrypt_context.to_dict()
- {'schemes': ['ldap_salted_sha1',
- 'ldap_salted_md5',
- 'ldap_sha1',
- 'ldap_md5',
- 'ldap_plaintext']}
-
- .. versionadded:: 1.6
- This was previously available as ``CryptContext().policy.to_dict()``
-
- .. seealso:: the :ref:`context-serialization-example` example in the tutorial.
- """
- # XXX: should resolve default to conditional behavior
- # based on presence of unregistered handlers?
- render_key = self._render_config_key
- return dict((render_key(key), value)
- for key, value in self._config.iter_config(resolve))
-
- def _write_to_parser(self, parser, section):
- """helper to write to ConfigParser instance"""
- render_key = self._render_config_key
- render_value = self._render_ini_value
- parser.add_section(section)
- for k,v in self._config.iter_config():
- v = render_value(k, v)
- k = render_key(k)
- parser.set(section, k, v)
-
- def to_string(self, section="passlib"):
- """serialize to INI format and return as unicode string.
-
- :param section:
- name of INI section to output, defaults to ``"passlib"``.
-
- :returns:
- CryptContext configuration, serialized to a INI unicode string.
-
- This function acts exactly like :meth:`to_dict`, except that it
- serializes all the contents into a single human-readable string,
- which can be hand edited, and/or stored in a file. The
- output of this method is accepted by :meth:`from_string`,
- :meth:`from_path`, and :meth:`load`. As an example::
-
- >>> # you can dump the configuration of any crypt context...
- >>> from passlib.apps import ldap_nocrypt_context
- >>> print ldap_nocrypt_context.to_string()
- [passlib]
- schemes = ldap_salted_sha1, ldap_salted_md5, ldap_sha1, ldap_md5, ldap_plaintext
-
- .. versionadded:: 1.6
- This was previously available as ``CryptContext().policy.to_string()``
-
- .. seealso:: the :ref:`context-serialization-example` example in the tutorial.
- """
- parser = SafeConfigParser()
- self._write_to_parser(parser, section)
- buf = NativeStringIO()
- parser.write(buf)
- unregistered = self._get_unregistered_handlers()
- if unregistered:
- buf.write((
- "# NOTE: the %s handler(s) are not registered with Passlib,\n"
- "# this string may not correctly reproduce the current configuration.\n\n"
- ) % ", ".join(repr(handler.name) for handler in unregistered))
- out = buf.getvalue()
- if not PY3:
- out = out.decode("utf-8")
- return out
-
- # XXX: is this useful enough to enable?
- ##def write_to_path(self, path, section="passlib", update=False):
- ## "write to INI file"
- ## parser = ConfigParser()
- ## if update and os.path.exists(path):
- ## if not parser.read([path]):
- ## raise EnvironmentError("failed to read existing file")
- ## parser.remove_section(section)
- ## self._write_to_parser(parser, section)
- ## fh = file(path, "w")
- ## parser.write(fh)
- ## fh.close()
-
- #===================================================================
- # verify() hardening
- # NOTE: this entire feature has been disabled.
- # all contents of this section are NOOPs as of 1.7.1,
- # and will be removed in 1.8.
- #===================================================================
-
- mvt_estimate_max_samples = 20
- mvt_estimate_min_samples = 10
- mvt_estimate_max_time = 2
- mvt_estimate_resolution = 0.01
- harden_verify = None
- min_verify_time = 0
-
- def reset_min_verify_time(self):
- self._reset_dummy_verify()
-
- #===================================================================
- # password hash api
- #===================================================================
-
- # NOTE: all the following methods do is look up the appropriate
- # custom handler for a given (scheme,category) combination,
- # and hand off the real work to the handler itself,
- # which is optimized for the specific (scheme,category) configuration.
- #
- # The custom handlers are cached inside the _CryptConfig
- # instance stored in self._config, and are retrieved
- # via get_record() and identify_record().
- #
- # _get_record() and _identify_record() are references
- # to _config methods of the same name,
- # stored in CryptContext for speed.
-
- def _get_or_identify_record(self, hash, scheme=None, category=None):
- """return record based on scheme, or failing that, by identifying hash"""
- if scheme:
- if not isinstance(hash, unicode_or_bytes_types):
- raise ExpectedStringError(hash, "hash")
- return self._get_record(scheme, category)
- else:
- # hash typecheck handled by identify_record()
- return self._identify_record(hash, category)
-
- def _strip_unused_context_kwds(self, kwds, record):
- """
- helper which removes any context keywords from **kwds**
- that are known to be used by another scheme in this context,
- but are NOT supported by handler specified by **record**.
-
- .. note::
- as optimization, load() will set this method to None on a per-instance basis
- if there are no context kwds.
- """
- if not kwds:
- return
- unused_kwds = self._config.context_kwds.difference(record.context_kwds)
- for key in unused_kwds:
- kwds.pop(key, None)
-
- def needs_update(self, hash, scheme=None, category=None, secret=None):
- """Check if hash needs to be replaced for some reason,
- in which case the secret should be re-hashed.
-
- This function is the core of CryptContext's support for hash migration:
- This function takes in a hash string, and checks the scheme,
- number of rounds, and other properties against the current policy.
- It returns ``True`` if the hash is using a deprecated scheme,
- or is otherwise outside of the bounds specified by the policy
- (e.g. the number of rounds is lower than :ref:`min_rounds <context-min-rounds-option>`
- configuration for that algorithm).
- If so, the password should be re-hashed using :meth:`hash`
- Otherwise, it will return ``False``.
-
- :type hash: unicode or bytes
- :arg hash:
- The hash string to examine.
-
- :type scheme: str or None
- :param scheme:
-
- Optional scheme to use. Scheme must be one of the ones
- configured for this context (see the
- :ref:`schemes <context-schemes-option>` option).
- If no scheme is specified, it will be identified
- based on the value of *hash*.
-
- .. deprecated:: 1.7
-
- Support for this keyword is deprecated, and will be removed in Passlib 2.0.
-
- :type category: str or None
- :param category:
- Optional :ref:`user category <user-categories>`.
- If specified, this will cause any category-specific defaults to
- be used when determining if the hash needs to be updated
- (e.g. is below the minimum rounds).
-
- :type secret: unicode, bytes, or None
- :param secret:
- Optional secret associated with the provided ``hash``.
- This is not required, or even currently used for anything...
- it's for forward-compatibility with any future
- update checks that might need this information.
- If provided, Passlib assumes the secret has already been
- verified successfully against the hash.
-
- .. versionadded:: 1.6
-
- :returns: ``True`` if hash should be replaced, otherwise ``False``.
-
- :raises ValueError:
- If the hash did not match any of the configured :meth:`schemes`.
-
- .. versionadded:: 1.6
- This method was previously named :meth:`hash_needs_update`.
-
- .. seealso:: the :ref:`context-migration-example` example in the tutorial.
- """
- if scheme is not None:
- # TODO: offer replacement alternative.
- # ``context.handler(scheme).needs_update()`` would work,
- # but may deprecate .handler() in passlib 1.8.
- warn("CryptContext.needs_update(): 'scheme' keyword is deprecated as of "
- "Passlib 1.7, and will be removed in Passlib 2.0",
- DeprecationWarning)
- record = self._get_or_identify_record(hash, scheme, category)
- return record.deprecated or record.needs_update(hash, secret=secret)
-
- @deprecated_method(deprecated="1.6", removed="2.0", replacement="CryptContext.needs_update()")
- def hash_needs_update(self, hash, scheme=None, category=None):
- """Legacy alias for :meth:`needs_update`.
-
- .. deprecated:: 1.6
- This method was renamed to :meth:`!needs_update` in version 1.6.
- This alias will be removed in version 2.0, and should only
- be used for compatibility with Passlib 1.3 - 1.5.
- """
- return self.needs_update(hash, scheme, category)
-
- @deprecated_method(deprecated="1.7", removed="2.0")
- def genconfig(self, scheme=None, category=None, **settings):
- """Generate a config string for specified scheme.
-
- .. deprecated:: 1.7
-
- This method will be removed in version 2.0, and should only
- be used for compatibility with Passlib 1.3 - 1.6.
- """
- record = self._get_record(scheme, category)
- strip_unused = self._strip_unused_context_kwds
- if strip_unused:
- strip_unused(settings, record)
- return record.genconfig(**settings)
-
- @deprecated_method(deprecated="1.7", removed="2.0")
- def genhash(self, secret, config, scheme=None, category=None, **kwds):
- """Generate hash for the specified secret using another hash.
-
- .. deprecated:: 1.7
-
- This method will be removed in version 2.0, and should only
- be used for compatibility with Passlib 1.3 - 1.6.
- """
- record = self._get_or_identify_record(config, scheme, category)
- strip_unused = self._strip_unused_context_kwds
- if strip_unused:
- strip_unused(kwds, record)
- return record.genhash(secret, config, **kwds)
-
- def identify(self, hash, category=None, resolve=False, required=False,
- unconfigured=False):
- """Attempt to identify which algorithm the hash belongs to.
-
- Note that this will only consider the algorithms
- currently configured for this context
- (see the :ref:`schemes <context-schemes-option>` option).
- All registered algorithms will be checked, from first to last,
- and whichever one positively identifies the hash first will be returned.
-
- :type hash: unicode or bytes
- :arg hash:
- The hash string to test.
-
- :type category: str or None
- :param category:
- Optional :ref:`user category <user-categories>`.
- Ignored by this function, this parameter
- is provided for symmetry with the other methods.
-
- :type resolve: bool
- :param resolve:
- If ``True``, returns the hash handler itself,
- instead of the name of the hash.
-
- :type required: bool
- :param required:
- If ``True``, this will raise a ValueError if the hash
- cannot be identified, instead of returning ``None``.
-
- :returns:
- The handler which first identifies the hash,
- or ``None`` if none of the algorithms identify the hash.
- """
- record = self._identify_record(hash, category, required)
- if record is None:
- return None
- elif resolve:
- if unconfigured:
- return record._Context__orig_handler
- else:
- return record
- else:
- return record.name
-
- def hash(self, secret, scheme=None, category=None, **kwds):
- """run secret through selected algorithm, returning resulting hash.
-
- :type secret: unicode or bytes
- :arg secret:
- the password to hash.
-
- :type scheme: str or None
- :param scheme:
-
- Optional scheme to use. Scheme must be one of the ones
- configured for this context (see the
- :ref:`schemes <context-schemes-option>` option).
- If no scheme is specified, the configured default
- will be used.
-
- .. deprecated:: 1.7
-
- Support for this keyword is deprecated, and will be removed in Passlib 2.0.
-
- :type category: str or None
- :param category:
- Optional :ref:`user category <user-categories>`.
- If specified, this will cause any category-specific defaults to
- be used when hashing the password (e.g. different default scheme,
- different default rounds values, etc).
-
- :param \*\*kwds:
- All other keyword options are passed to the selected algorithm's
- :meth:`PasswordHash.hash() <passlib.ifc.PasswordHash.hash>` method.
-
- :returns:
- The secret as encoded by the specified algorithm and options.
- The return value will always be a :class:`!str`.
-
- :raises TypeError, ValueError:
- * If any of the arguments have an invalid type or value.
- This includes any keywords passed to the underlying hash's
- :meth:`PasswordHash.hash() <passlib.ifc.PasswordHash.hash>` method.
-
- .. seealso:: the :ref:`context-basic-example` example in the tutorial
- """
- # XXX: could insert normalization to preferred unicode encoding here
- if scheme is not None:
- # TODO: offer replacement alternative.
- # ``context.handler(scheme).hash()`` would work,
- # but may deprecate .handler() in passlib 1.8.
- warn("CryptContext.hash(): 'scheme' keyword is deprecated as of "
- "Passlib 1.7, and will be removed in Passlib 2.0",
- DeprecationWarning)
- record = self._get_record(scheme, category)
- strip_unused = self._strip_unused_context_kwds
- if strip_unused:
- strip_unused(kwds, record)
- return record.hash(secret, **kwds)
-
- @deprecated_method(deprecated="1.7", removed="2.0", replacement="CryptContext.hash()")
- def encrypt(self, *args, **kwds):
- """
- Legacy alias for :meth:`hash`.
-
- .. deprecated:: 1.7
- This method was renamed to :meth:`!hash` in version 1.7.
- This alias will be removed in version 2.0, and should only
- be used for compatibility with Passlib 1.3 - 1.6.
- """
- return self.hash(*args, **kwds)
-
- def verify(self, secret, hash, scheme=None, category=None, **kwds):
- """verify secret against an existing hash.
-
- If no scheme is specified, this will attempt to identify
- the scheme based on the contents of the provided hash
- (limited to the schemes configured for this context).
- It will then check whether the password verifies against the hash.
-
- :type secret: unicode or bytes
- :arg secret:
- the secret to verify
-
- :type hash: unicode or bytes
- :arg hash:
- hash string to compare to
-
- if ``None`` is passed in, this will be treated as "never verifying"
-
- :type scheme: str
- :param scheme:
- Optionally force context to use specific scheme.
- This is usually not needed, as most hashes can be unambiguously
- identified. Scheme must be one of the ones configured
- for this context
- (see the :ref:`schemes <context-schemes-option>` option).
-
- .. deprecated:: 1.7
-
- Support for this keyword is deprecated, and will be removed in Passlib 2.0.
-
- :type category: str or None
- :param category:
- Optional :ref:`user category <user-categories>` string.
- This is mainly used when generating new hashes, it has little
- effect when verifying; this keyword is mainly provided for symmetry.
-
- :param \*\*kwds:
- All additional keywords are passed to the appropriate handler,
- and should match its :attr:`~passlib.ifc.PasswordHash.context_kwds`.
-
- :returns:
- ``True`` if the password matched the hash, else ``False``.
-
- :raises ValueError:
- * if the hash did not match any of the configured :meth:`schemes`.
-
- * if any of the arguments have an invalid value (this includes
- any keywords passed to the underlying hash's
- :meth:`PasswordHash.verify() <passlib.ifc.PasswordHash.verify>` method).
-
- :raises TypeError:
- * if any of the arguments have an invalid type (this includes
- any keywords passed to the underlying hash's
- :meth:`PasswordHash.verify() <passlib.ifc.PasswordHash.verify>` method).
-
- .. seealso:: the :ref:`context-basic-example` example in the tutorial
- """
- # XXX: could insert normalization to preferred unicode encoding here
- # XXX: what about supporting a setter() callback ala django 1.4 ?
- if scheme is not None:
- # TODO: offer replacement alternative.
- # ``context.handler(scheme).verify()`` would work,
- # but may deprecate .handler() in passlib 1.8.
- warn("CryptContext.verify(): 'scheme' keyword is deprecated as of "
- "Passlib 1.7, and will be removed in Passlib 2.0",
- DeprecationWarning)
- if hash is None:
- # convenience feature -- let apps pass in hash=None when user
- # isn't found / has no hash; useful because it invokes dummy_verify()
- self.dummy_verify()
- return False
- record = self._get_or_identify_record(hash, scheme, category)
- strip_unused = self._strip_unused_context_kwds
- if strip_unused:
- strip_unused(kwds, record)
- return record.verify(secret, hash, **kwds)
-
- def verify_and_update(self, secret, hash, scheme=None, category=None, **kwds):
- """verify password and re-hash the password if needed, all in a single call.
-
- This is a convenience method which takes care of all the following:
- first it verifies the password (:meth:`~CryptContext.verify`), if this is successfull
- it checks if the hash needs updating (:meth:`~CryptContext.needs_update`), and if so,
- re-hashes the password (:meth:`~CryptContext.hash`), returning the replacement hash.
- This series of steps is a very common task for applications
- which wish to update deprecated hashes, and this call takes
- care of all 3 steps efficiently.
-
- :type secret: unicode or bytes
- :arg secret:
- the secret to verify
-
- :type secret: unicode or bytes
- :arg hash:
- hash string to compare to.
-
- if ``None`` is passed in, this will be treated as "never verifying"
-
- :type scheme: str
- :param scheme:
- Optionally force context to use specific scheme.
- This is usually not needed, as most hashes can be unambiguously
- identified. Scheme must be one of the ones configured
- for this context
- (see the :ref:`schemes <context-schemes-option>` option).
-
- .. deprecated:: 1.7
-
- Support for this keyword is deprecated, and will be removed in Passlib 2.0.
-
- :type category: str or None
- :param category:
- Optional :ref:`user category <user-categories>`.
- If specified, this will cause any category-specific defaults to
- be used if the password has to be re-hashed.
-
- :param \*\*kwds:
- all additional keywords are passed to the appropriate handler,
- and should match that hash's
- :attr:`PasswordHash.context_kwds <passlib.ifc.PasswordHash.context_kwds>`.
-
- :returns:
- This function returns a tuple containing two elements:
- ``(verified, replacement_hash)``. The first is a boolean
- flag indicating whether the password verified,
- and the second an optional replacement hash.
- The tuple will always match one of the following 3 cases:
-
- * ``(False, None)`` indicates the secret failed to verify.
- * ``(True, None)`` indicates the secret verified correctly,
- and the hash does not need updating.
- * ``(True, str)`` indicates the secret verified correctly,
- but the current hash needs to be updated. The :class:`!str`
- will be the freshly generated hash, to replace the old one.
-
- :raises TypeError, ValueError:
- For the same reasons as :meth:`verify`.
-
- .. seealso:: the :ref:`context-migration-example` example in the tutorial.
- """
- # XXX: could insert normalization to preferred unicode encoding here.
- if scheme is not None:
- warn("CryptContext.verify(): 'scheme' keyword is deprecated as of "
- "Passlib 1.7, and will be removed in Passlib 2.0",
- DeprecationWarning)
- if hash is None:
- # convenience feature -- let apps pass in hash=None when user
- # isn't found / has no hash; useful because it invokes dummy_verify()
- self.dummy_verify()
- return False, None
- record = self._get_or_identify_record(hash, scheme, category)
- strip_unused = self._strip_unused_context_kwds
- if strip_unused and kwds:
- clean_kwds = kwds.copy()
- strip_unused(clean_kwds, record)
- else:
- clean_kwds = kwds
- # XXX: if record is default scheme, could extend PasswordHash
- # api to combine verify & needs_update to single call,
- # potentially saving some round-trip parsing.
- # but might make these codepaths more complex...
- if not record.verify(secret, hash, **clean_kwds):
- return False, None
- elif record.deprecated or record.needs_update(hash, secret=secret):
- # NOTE: we re-hash with default scheme, not current one.
- return True, self.hash(secret, category=category, **kwds)
- else:
- return True, None
-
- #===================================================================
- # missing-user helper
- #===================================================================
-
- #: secret used for dummy_verify()
- _dummy_secret = "too many secrets"
-
- @memoized_property
- def _dummy_hash(self):
- """
- precalculated hash for dummy_verify() to use
- """
- return self.hash(self._dummy_secret)
-
- def _reset_dummy_verify(self):
- """
- flush memoized values used by dummy_verify()
- """
- type(self)._dummy_hash.clear_cache(self)
-
- def dummy_verify(self, elapsed=0):
- """
- Helper that applications can call when user wasn't found,
- in order to simulate time it would take to hash a password.
-
- Runs verify() against a dummy hash, to simulate verification
- of a real account password.
-
- :param elapsed:
-
- .. deprecated:: 1.7.1
-
- this option is ignored, and will be removed in passlib 1.8.
-
- .. versionadded:: 1.7
- """
- self.verify(self._dummy_secret, self._dummy_hash)
- return False
-
- #===================================================================
- # disabled hash support
- #===================================================================
-
- def is_enabled(self, hash):
- """
- test if hash represents a usuable password --
- i.e. does not represent an unusuable password such as ``"!"``,
- which is recognized by the :class:`~passlib.hash.unix_disabled` hash.
-
- :raises ValueError:
- if the hash is not recognized
- (typically solved by adding ``unix_disabled`` to the list of schemes).
- """
- return not self._identify_record(hash, None).is_disabled
-
- def disable(self, hash=None):
- """
- return a string to disable logins for user,
- usually by returning a non-verifying string such as ``"!"``.
-
- :param hash:
- Callers can optionally provide the account's existing hash.
- Some disabled handlers (such as :class:`!unix_disabled`)
- will encode this into the returned value,
- so that it can be recovered via :meth:`enable`.
-
- :raises RuntimeError:
- if this function is called w/o a disabled hasher
- (such as :class:`~passlib.hash.unix_disabled`) included
- in the list of schemes.
-
- :returns:
- hash string which will be recognized as valid by the context,
- but is guaranteed to not validate against *any* password.
- """
- record = self._config.disabled_record
- assert record.is_disabled
- return record.disable(hash)
-
- def enable(self, hash):
- """
- inverse of :meth:`disable` --
- attempts to recover original hash which was converted
- by a :meth:`!disable` call into a disabled hash --
- thus restoring the user's original password.
-
- :raises ValueError:
- if original hash not present, or if the disabled handler doesn't
- support encoding the original hash (e.g. ``django_disabled``)
-
- :returns:
- the original hash.
- """
- record = self._identify_record(hash, None)
- if record.is_disabled:
- # XXX: should we throw error if result can't be identified by context?
- return record.enable(hash)
- else:
- # hash wasn't a disabled hash, so return unchanged
- return hash
-
- #===================================================================
- # eoc
- #===================================================================
-
- class LazyCryptContext(CryptContext):
- """CryptContext subclass which doesn't load handlers until needed.
-
- This is a subclass of CryptContext which takes in a set of arguments
- exactly like CryptContext, but won't import any handlers
- (or even parse its arguments) until
- the first time one of its methods is accessed.
-
- :arg schemes:
- The first positional argument can be a list of schemes, or omitted,
- just like CryptContext.
-
- :param onload:
-
- If a callable is passed in via this keyword,
- it will be invoked at lazy-load time
- with the following signature:
- ``onload(**kwds) -> kwds``;
- where ``kwds`` is all the additional kwds passed to LazyCryptContext.
- It should perform any additional deferred initialization,
- and return the final dict of options to be passed to CryptContext.
-
- .. versionadded:: 1.6
-
- :param create_policy:
-
- .. deprecated:: 1.6
- This option will be removed in Passlib 1.8,
- applications should use ``onload`` instead.
-
- :param kwds:
-
- All additional keywords are passed to CryptContext;
- or to the *onload* function (if provided).
-
- This is mainly used internally by modules such as :mod:`passlib.apps`,
- which define a large number of contexts, but only a few of them will be needed
- at any one time. Use of this class saves the memory needed to import
- the specified handlers until the context instance is actually accessed.
- As well, it allows constructing a context at *module-init* time,
- but using :func:`!onload()` to provide dynamic configuration
- at *application-run* time.
-
- .. note::
- This class is only useful if you're referencing handler objects by name,
- and don't want them imported until runtime. If you want to have the config
- validated before your application runs, or are passing in already-imported
- handler instances, you should use :class:`CryptContext` instead.
-
- .. versionadded:: 1.4
- """
- _lazy_kwds = None
-
- # NOTE: the way this class works changed in 1.6.
- # previously it just called _lazy_init() when ``.policy`` was
- # first accessed. now that is done whenever any of the public
- # attributes are accessed, and the class itself is changed
- # to a regular CryptContext, to remove the overhead once it's unneeded.
-
- def __init__(self, schemes=None, **kwds):
- if schemes is not None:
- kwds['schemes'] = schemes
- self._lazy_kwds = kwds
-
- def _lazy_init(self):
- kwds = self._lazy_kwds
- if 'create_policy' in kwds:
- warn("The CryptPolicy class, and LazyCryptContext's "
- "``create_policy`` keyword have been deprecated as of "
- "Passlib 1.6, and will be removed in Passlib 1.8; "
- "please use the ``onload`` keyword instead.",
- DeprecationWarning)
- create_policy = kwds.pop("create_policy")
- result = create_policy(**kwds)
- policy = CryptPolicy.from_source(result, _warn=False)
- kwds = policy._context.to_dict()
- elif 'onload' in kwds:
- onload = kwds.pop("onload")
- kwds = onload(**kwds)
- del self._lazy_kwds
- super(LazyCryptContext, self).__init__(**kwds)
- self.__class__ = CryptContext
-
- def __getattribute__(self, attr):
- if (not attr.startswith("_") or attr.startswith("__")) and \
- self._lazy_kwds is not None:
- self._lazy_init()
- return object.__getattribute__(self, attr)
-
- #=============================================================================
- # eof
- #=============================================================================
|