From 2d6a2bd435d422bfa0e51ee91fcd32ea89b2091f Mon Sep 17 00:00:00 2001 From: Polina Date: Thu, 2 Jul 2026 18:53:51 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?venv/Lib/site-packages/passlib=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- venv/Lib/site-packages/passlib/__init__.py | 3 + venv/Lib/site-packages/passlib/apache.py | 1255 ++++++++++ venv/Lib/site-packages/passlib/apps.py | 245 ++ venv/Lib/site-packages/passlib/context.py | 2637 ++++++++++++++++++++ venv/Lib/site-packages/passlib/exc.py | 397 +++ 5 files changed, 4537 insertions(+) create mode 100644 venv/Lib/site-packages/passlib/__init__.py create mode 100644 venv/Lib/site-packages/passlib/apache.py create mode 100644 venv/Lib/site-packages/passlib/apps.py create mode 100644 venv/Lib/site-packages/passlib/context.py create mode 100644 venv/Lib/site-packages/passlib/exc.py diff --git a/venv/Lib/site-packages/passlib/__init__.py b/venv/Lib/site-packages/passlib/__init__.py new file mode 100644 index 0000000..963bfcc --- /dev/null +++ b/venv/Lib/site-packages/passlib/__init__.py @@ -0,0 +1,3 @@ +"""passlib - suite of password hashing & generation routines""" + +__version__ = '1.7.4' diff --git a/venv/Lib/site-packages/passlib/apache.py b/venv/Lib/site-packages/passlib/apache.py new file mode 100644 index 0000000..a75f2cf --- /dev/null +++ b/venv/Lib/site-packages/passlib/apache.py @@ -0,0 +1,1255 @@ +"""passlib.apache - apache password support""" +# XXX: relocate this to passlib.ext.apache? +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +import os +from warnings import warn +# site +# pkg +from passlib import exc, registry +from passlib.context import CryptContext +from passlib.exc import ExpectedStringError +from passlib.hash import htdigest +from passlib.utils import render_bytes, to_bytes, is_ascii_codec +from passlib.utils.decor import deprecated_method +from passlib.utils.compat import join_bytes, unicode, BytesIO, PY3 +# local +__all__ = [ + 'HtpasswdFile', + 'HtdigestFile', +] + +#============================================================================= +# constants & support +#============================================================================= +_UNSET = object() + +_BCOLON = b":" +_BHASH = b"#" + +# byte values that aren't allowed in fields. +_INVALID_FIELD_CHARS = b":\n\r\t\x00" + +#: _CommonFile._source token types +_SKIPPED = "skipped" +_RECORD = "record" + +#============================================================================= +# common helpers +#============================================================================= +class _CommonFile(object): + """common framework for HtpasswdFile & HtdigestFile""" + #=================================================================== + # instance attrs + #=================================================================== + + # charset encoding used by file (defaults to utf-8) + encoding = None + + # whether users() and other public methods should return unicode or bytes? + # (defaults to False under PY2, True under PY3) + return_unicode = None + + # if bound to local file, these will be set. + _path = None # local file path + _mtime = None # mtime when last loaded, or 0 + + # if true, automatically save to local file after changes are made. + autosave = False + + # dict mapping key -> value for all records in database. + # (e.g. user => hash for Htpasswd) + _records = None + + #: list of tokens for recreating original file contents when saving. if present, + #: will be sequence of (_SKIPPED, b"whitespace/comments") and (_RECORD, ) tuples. + _source = None + + #=================================================================== + # alt constuctors + #=================================================================== + @classmethod + def from_string(cls, data, **kwds): + """create new object from raw string. + + :type data: unicode or bytes + :arg data: + database to load, as single string. + + :param \\*\\*kwds: + all other keywords are the same as in the class constructor + """ + if 'path' in kwds: + raise TypeError("'path' not accepted by from_string()") + self = cls(**kwds) + self.load_string(data) + return self + + @classmethod + def from_path(cls, path, **kwds): + """create new object from file, without binding object to file. + + :type path: str + :arg path: + local filepath to load from + + :param \\*\\*kwds: + all other keywords are the same as in the class constructor + """ + self = cls(**kwds) + self.load(path) + return self + + #=================================================================== + # init + #=================================================================== + def __init__(self, path=None, new=False, autoload=True, autosave=False, + encoding="utf-8", return_unicode=PY3, + ): + # set encoding + if not encoding: + warn("``encoding=None`` is deprecated as of Passlib 1.6, " + "and will cause a ValueError in Passlib 1.8, " + "use ``return_unicode=False`` instead.", + DeprecationWarning, stacklevel=2) + encoding = "utf-8" + return_unicode = False + elif not is_ascii_codec(encoding): + # htpasswd/htdigest files assumes 1-byte chars, and use ":" separator, + # so only ascii-compatible encodings are allowed. + raise ValueError("encoding must be 7-bit ascii compatible") + self.encoding = encoding + + # set other attrs + self.return_unicode = return_unicode + self.autosave = autosave + self._path = path + self._mtime = 0 + + # init db + if not autoload: + warn("``autoload=False`` is deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8, use ``new=True`` instead", + DeprecationWarning, stacklevel=2) + new = True + if path and not new: + self.load() + else: + self._records = {} + self._source = [] + + def __repr__(self): + tail = '' + if self.autosave: + tail += ' autosave=True' + if self._path: + tail += ' path=%r' % self._path + if self.encoding != "utf-8": + tail += ' encoding=%r' % self.encoding + return "<%s 0x%0x%s>" % (self.__class__.__name__, id(self), tail) + + # NOTE: ``path`` is a property so that ``_mtime`` is wiped when it's set. + + @property + def path(self): + return self._path + + @path.setter + def path(self, value): + if value != self._path: + self._mtime = 0 + self._path = value + + @property + def mtime(self): + """modify time when last loaded (if bound to a local file)""" + return self._mtime + + #=================================================================== + # loading + #=================================================================== + def load_if_changed(self): + """Reload from ``self.path`` only if file has changed since last load""" + if not self._path: + raise RuntimeError("%r is not bound to a local file" % self) + if self._mtime and self._mtime == os.path.getmtime(self._path): + return False + self.load() + return True + + def load(self, path=None, force=True): + """Load state from local file. + If no path is specified, attempts to load from ``self.path``. + + :type path: str + :arg path: local file to load from + + :type force: bool + :param force: + if ``force=False``, only load from ``self.path`` if file + has changed since last load. + + .. deprecated:: 1.6 + This keyword will be removed in Passlib 1.8; + Applications should use :meth:`load_if_changed` instead. + """ + if path is not None: + with open(path, "rb") as fh: + self._mtime = 0 + self._load_lines(fh) + elif not force: + warn("%(name)s.load(force=False) is deprecated as of Passlib 1.6," + "and will be removed in Passlib 1.8; " + "use %(name)s.load_if_changed() instead." % + dict(name=self.__class__.__name__), + DeprecationWarning, stacklevel=2) + return self.load_if_changed() + elif self._path: + with open(self._path, "rb") as fh: + self._mtime = os.path.getmtime(self._path) + self._load_lines(fh) + else: + raise RuntimeError("%s().path is not set, an explicit path is required" % + self.__class__.__name__) + return True + + def load_string(self, data): + """Load state from unicode or bytes string, replacing current state""" + data = to_bytes(data, self.encoding, "data") + self._mtime = 0 + self._load_lines(BytesIO(data)) + + def _load_lines(self, lines): + """load from sequence of lists""" + parse = self._parse_record + records = {} + source = [] + skipped = b'' + for idx, line in enumerate(lines): + # NOTE: per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c), + # lines with only whitespace, or with "#" as first non-whitespace char, + # are left alone / ignored. + tmp = line.lstrip() + if not tmp or tmp.startswith(_BHASH): + skipped += line + continue + + # parse valid line + key, value = parse(line, idx+1) + + # NOTE: if multiple entries for a key, we use the first one, + # which seems to match htpasswd source + if key in records: + log.warning("username occurs multiple times in source file: %r" % key) + skipped += line + continue + + # flush buffer of skipped whitespace lines + if skipped: + source.append((_SKIPPED, skipped)) + skipped = b'' + + # store new user line + records[key] = value + source.append((_RECORD, key)) + + # don't bother preserving trailing whitespace, but do preserve trailing comments + if skipped.rstrip(): + source.append((_SKIPPED, skipped)) + + # NOTE: not replacing ._records until parsing succeeds, so loading is atomic. + self._records = records + self._source = source + + def _parse_record(self, record, lineno): # pragma: no cover - abstract method + """parse line of file into (key, value) pair""" + raise NotImplementedError("should be implemented in subclass") + + def _set_record(self, key, value): + """ + helper for setting record which takes care of inserting source line if needed; + + :returns: + bool if key already present + """ + records = self._records + existing = (key in records) + records[key] = value + if not existing: + self._source.append((_RECORD, key)) + return existing + + #=================================================================== + # saving + #=================================================================== + def _autosave(self): + """subclass helper to call save() after any changes""" + if self.autosave and self._path: + self.save() + + def save(self, path=None): + """Save current state to file. + If no path is specified, attempts to save to ``self.path``. + """ + if path is not None: + with open(path, "wb") as fh: + fh.writelines(self._iter_lines()) + elif self._path: + self.save(self._path) + self._mtime = os.path.getmtime(self._path) + else: + raise RuntimeError("%s().path is not set, cannot autosave" % + self.__class__.__name__) + + def to_string(self): + """Export current state as a string of bytes""" + return join_bytes(self._iter_lines()) + + # def clean(self): + # """ + # discard any comments or whitespace that were being preserved from the source file, + # and re-sort keys in alphabetical order + # """ + # self._source = [(_RECORD, key) for key in sorted(self._records)] + # self._autosave() + + def _iter_lines(self): + """iterator yielding lines of database""" + # NOTE: this relies on being an OrderedDict so that it outputs + # records in a deterministic order. + records = self._records + if __debug__: + pending = set(records) + for action, content in self._source: + if action == _SKIPPED: + # 'content' is whitespace/comments to write + yield content + else: + assert action == _RECORD + # 'content' is record key + if content not in records: + # record was deleted + # NOTE: doing it lazily like this so deleting & re-adding user + # preserves their original location in the file. + continue + yield self._render_record(content, records[content]) + if __debug__: + pending.remove(content) + if __debug__: + # sanity check that we actually wrote all the records + # (otherwise _source & _records are somehow out of sync) + assert not pending, "failed to write all records: missing=%r" % (pending,) + + def _render_record(self, key, value): # pragma: no cover - abstract method + """given key/value pair, encode as line of file""" + raise NotImplementedError("should be implemented in subclass") + + #=================================================================== + # field encoding + #=================================================================== + def _encode_user(self, user): + """user-specific wrapper for _encode_field()""" + return self._encode_field(user, "user") + + def _encode_realm(self, realm): # pragma: no cover - abstract method + """realm-specific wrapper for _encode_field()""" + return self._encode_field(realm, "realm") + + def _encode_field(self, value, param="field"): + """convert field to internal representation. + + internal representation is always bytes. byte strings are left as-is, + unicode strings encoding using file's default encoding (or ``utf-8`` + if no encoding has been specified). + + :raises UnicodeEncodeError: + if unicode value cannot be encoded using default encoding. + + :raises ValueError: + if resulting byte string contains a forbidden character, + or is too long (>255 bytes). + + :returns: + encoded identifer as bytes + """ + if isinstance(value, unicode): + value = value.encode(self.encoding) + elif not isinstance(value, bytes): + raise ExpectedStringError(value, param) + if len(value) > 255: + raise ValueError("%s must be at most 255 characters: %r" % + (param, value)) + if any(c in _INVALID_FIELD_CHARS for c in value): + raise ValueError("%s contains invalid characters: %r" % + (param, value,)) + return value + + def _decode_field(self, value): + """decode field from internal representation to format + returns by users() method, etc. + + :raises UnicodeDecodeError: + if unicode value cannot be decoded using default encoding. + (usually indicates wrong encoding set for file). + + :returns: + field as unicode or bytes, as appropriate. + """ + assert isinstance(value, bytes), "expected value to be bytes" + if self.return_unicode: + return value.decode(self.encoding) + else: + return value + + # FIXME: htpasswd doc says passwords limited to 255 chars under Windows & MPE, + # and that longer ones are truncated. this may be side-effect of those + # platforms supporting the 'plaintext' scheme. these classes don't currently + # check for this. + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# htpasswd context +# +# This section sets up a CryptContexts to mimic what schemes Apache +# (and the htpasswd tool) should support on the current system. +# +# Apache has long-time supported some basic builtin schemes (listed below), +# as well as the host's crypt() method -- though it's limited to being able +# to *verify* any scheme using that method, but can only generate "des_crypt" hashes. +# +# Apache 2.4 added builtin bcrypt support (even for platforms w/o native support). +# c.f. http://httpd.apache.org/docs/2.4/programs/htpasswd.html vs the 2.2 docs. +#============================================================================= + +#: set of default schemes that (if chosen) should be using bcrypt, +#: but can't due to lack of bcrypt. +_warn_no_bcrypt = set() + +def _init_default_schemes(): + + #: pick strongest one for host + host_best = None + for name in ["bcrypt", "sha256_crypt"]: + if registry.has_os_crypt_support(name): + host_best = name + break + + # check if we have a bcrypt backend -- otherwise issue warning + # XXX: would like to not spam this unless the user *requests* apache 24 + bcrypt = "bcrypt" if registry.has_backend("bcrypt") else None + _warn_no_bcrypt.clear() + if not bcrypt: + _warn_no_bcrypt.update(["portable_apache_24", "host_apache_24", + "linux_apache_24", "portable", "host"]) + + defaults = dict( + # strongest hash builtin to specific apache version + portable_apache_24=bcrypt or "apr_md5_crypt", + portable_apache_22="apr_md5_crypt", + + # strongest hash across current host & specific apache version + host_apache_24=bcrypt or host_best or "apr_md5_crypt", + host_apache_22=host_best or "apr_md5_crypt", + + # strongest hash on a linux host + linux_apache_24=bcrypt or "sha256_crypt", + linux_apache_22="sha256_crypt", + ) + + # set latest-apache version aliases + # XXX: could check for apache install, and pick correct host 22/24 default? + # could reuse _detect_htpasswd() helper in UTs + defaults.update( + portable=defaults['portable_apache_24'], + host=defaults['host_apache_24'], + ) + return defaults + +#: dict mapping default alias -> appropriate scheme +htpasswd_defaults = _init_default_schemes() + +def _init_htpasswd_context(): + + # start with schemes built into apache + schemes = [ + # builtin support added in apache 2.4 + # (https://bz.apache.org/bugzilla/show_bug.cgi?id=49288) + "bcrypt", + + # support not "builtin" to apache, instead it requires support through host's crypt(). + # adding them here to allow editing htpasswd under windows and then deploying under unix. + "sha256_crypt", + "sha512_crypt", + "des_crypt", + + # apache default as of 2.2.18, and still default in 2.4 + "apr_md5_crypt", + + # NOTE: apache says ONLY intended for transitioning htpasswd <-> ldap + "ldap_sha1", + + # NOTE: apache says ONLY supported on Windows, Netware, TPF + "plaintext" + ] + + # apache can verify anything supported by the native crypt(), + # though htpasswd tool can only generate a limited set of hashes. + # (this list may overlap w/ builtin apache schemes) + schemes.extend(registry.get_supported_os_crypt_schemes()) + + # hack to remove dups and sort into preferred order + preferred = schemes[:3] + ["apr_md5_crypt"] + schemes + schemes = sorted(set(schemes), key=preferred.index) + + # create context object + return CryptContext( + schemes=schemes, + + # NOTE: default will change to "portable" in passlib 2.0 + default=htpasswd_defaults['portable_apache_22'], + + # NOTE: bcrypt "2y" is required, "2b" isn't recognized by libapr (issue 95) + bcrypt__ident="2y", + ) + +#: CryptContext configured to match htpasswd +htpasswd_context = _init_htpasswd_context() + +#============================================================================= +# htpasswd editing +#============================================================================= + +class HtpasswdFile(_CommonFile): + """class for reading & writing Htpasswd files. + + The class constructor accepts the following arguments: + + :type path: filepath + :param path: + + Specifies path to htpasswd file, use to implicitly load from and save to. + + This class has two modes of operation: + + 1. It can be "bound" to a local file by passing a ``path`` to the class + constructor. In this case it will load the contents of the file when + created, and the :meth:`load` and :meth:`save` methods will automatically + load from and save to that file if they are called without arguments. + + 2. Alternately, it can exist as an independant object, in which case + :meth:`load` and :meth:`save` will require an explicit path to be + provided whenever they are called. As well, ``autosave`` behavior + will not be available. + + This feature is new in Passlib 1.6, and is the default if no + ``path`` value is provided to the constructor. + + This is also exposed as a readonly instance attribute. + + :type new: bool + :param new: + + Normally, if *path* is specified, :class:`HtpasswdFile` will + immediately load the contents of the file. However, when creating + a new htpasswd file, applications can set ``new=True`` so that + the existing file (if any) will not be loaded. + + .. versionadded:: 1.6 + This feature was previously enabled by setting ``autoload=False``. + That alias has been deprecated, and will be removed in Passlib 1.8 + + :type autosave: bool + :param autosave: + + Normally, any changes made to an :class:`HtpasswdFile` instance + will not be saved until :meth:`save` is explicitly called. However, + if ``autosave=True`` is specified, any changes made will be + saved to disk immediately (assuming *path* has been set). + + This is also exposed as a writeable instance attribute. + + :type encoding: str + :param encoding: + + Optionally specify character encoding used to read/write file + and hash passwords. Defaults to ``utf-8``, though ``latin-1`` + is the only other commonly encountered encoding. + + This is also exposed as a readonly instance attribute. + + :type default_scheme: str + :param default_scheme: + Optionally specify default scheme to use when encoding new passwords. + + This can be any of the schemes with builtin Apache support, + OR natively supported by the host OS's :func:`crypt.crypt` function. + + * Builtin schemes include ``"bcrypt"`` (apache 2.4+), ``"apr_md5_crypt"`, + and ``"des_crypt"``. + + * Schemes commonly supported by Unix hosts + include ``"bcrypt"``, ``"sha256_crypt"``, and ``"des_crypt"``. + + In order to not have to sort out what you should use, + passlib offers a number of aliases, that will resolve + to the most appropriate scheme based on your needs: + + * ``"portable"``, ``"portable_apache_24"`` -- pick scheme that's portable across hosts + running apache >= 2.4. **This will be the default as of Passlib 2.0**. + + * ``"portable_apache_22"`` -- pick scheme that's portable across hosts + running apache >= 2.4. **This is the default up to Passlib 1.9**. + + * ``"host"``, ``"host_apache_24"`` -- pick strongest scheme supported by + apache >= 2.4 and/or host OS. + + * ``"host_apache_22"`` -- pick strongest scheme supported by + apache >= 2.2 and/or host OS. + + .. versionadded:: 1.6 + This keyword was previously named ``default``. That alias + has been deprecated, and will be removed in Passlib 1.8. + + .. versionchanged:: 1.6.3 + + Added support for ``"bcrypt"``, ``"sha256_crypt"``, and ``"portable"`` alias. + + .. versionchanged:: 1.7 + + Added apache 2.4 semantics, and additional aliases. + + :type context: :class:`~passlib.context.CryptContext` + :param context: + :class:`!CryptContext` instance used to create + and verify the hashes found in the htpasswd file. + The default value is a pre-built context which supports all + of the hashes officially allowed in an htpasswd file. + + This is also exposed as a readonly instance attribute. + + .. warning:: + + This option may be used to add support for non-standard hash + formats to an htpasswd file. However, the resulting file + will probably not be usable by another application, + and particularly not by Apache. + + :param autoload: + Set to ``False`` to prevent the constructor from automatically + loaded the file from disk. + + .. deprecated:: 1.6 + This has been replaced by the *new* keyword. + Instead of setting ``autoload=False``, you should use + ``new=True``. Support for this keyword will be removed + in Passlib 1.8. + + :param default: + Change the default algorithm used to hash new passwords. + + .. deprecated:: 1.6 + This has been renamed to *default_scheme* for clarity. + Support for this alias will be removed in Passlib 1.8. + + Loading & Saving + ================ + .. automethod:: load + .. automethod:: load_if_changed + .. automethod:: load_string + .. automethod:: save + .. automethod:: to_string + + Inspection + ================ + .. automethod:: users + .. automethod:: check_password + .. automethod:: get_hash + + Modification + ================ + .. automethod:: set_password + .. automethod:: delete + + Alternate Constructors + ====================== + .. automethod:: from_string + + Attributes + ========== + .. attribute:: path + + Path to local file that will be used as the default + for all :meth:`load` and :meth:`save` operations. + May be written to, initialized by the *path* constructor keyword. + + .. attribute:: autosave + + Writeable flag indicating whether changes will be automatically + written to *path*. + + Errors + ====== + :raises ValueError: + All of the methods in this class will raise a :exc:`ValueError` if + any user name contains a forbidden character (one of ``:\\r\\n\\t\\x00``), + or is longer than 255 characters. + """ + #=================================================================== + # instance attrs + #=================================================================== + + # NOTE: _records map stores for the key, and for the value, + # both in bytes which use self.encoding + + #=================================================================== + # init & serialization + #=================================================================== + def __init__(self, path=None, default_scheme=None, context=htpasswd_context, + **kwds): + if 'default' in kwds: + warn("``default`` is deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8, it has been renamed " + "to ``default_scheem``.", + DeprecationWarning, stacklevel=2) + default_scheme = kwds.pop("default") + if default_scheme: + if default_scheme in _warn_no_bcrypt: + warn("HtpasswdFile: no bcrypt backends available, " + "using fallback for default scheme %r" % default_scheme, + exc.PasslibSecurityWarning) + default_scheme = htpasswd_defaults.get(default_scheme, default_scheme) + context = context.copy(default=default_scheme) + self.context = context + super(HtpasswdFile, self).__init__(path, **kwds) + + def _parse_record(self, record, lineno): + # NOTE: should return (user, hash) tuple + result = record.rstrip().split(_BCOLON) + if len(result) != 2: + raise ValueError("malformed htpasswd file (error reading line %d)" + % lineno) + return result + + def _render_record(self, user, hash): + return render_bytes("%s:%s\n", user, hash) + + #=================================================================== + # public methods + #=================================================================== + + def users(self): + """ + Return list of all users in database + """ + return [self._decode_field(user) for user in self._records] + + ##def has_user(self, user): + ## "check whether entry is present for user" + ## return self._encode_user(user) in self._records + + ##def rename(self, old, new): + ## """rename user account""" + ## old = self._encode_user(old) + ## new = self._encode_user(new) + ## hash = self._records.pop(old) + ## self._records[new] = hash + ## self._autosave() + + def set_password(self, user, password): + """Set password for user; adds user if needed. + + :returns: + * ``True`` if existing user was updated. + * ``False`` if user account was added. + + .. versionchanged:: 1.6 + This method was previously called ``update``, it was renamed + to prevent ambiguity with the dictionary method. + The old alias is deprecated, and will be removed in Passlib 1.8. + """ + hash = self.context.hash(password) + return self.set_hash(user, hash) + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="set_password") + def update(self, user, password): + """set password for user""" + return self.set_password(user, password) + + def get_hash(self, user): + """Return hash stored for user, or ``None`` if user not found. + + .. versionchanged:: 1.6 + This method was previously named ``find``, it was renamed + for clarity. The old name is deprecated, and will be removed + in Passlib 1.8. + """ + try: + return self._records[self._encode_user(user)] + except KeyError: + return None + + def set_hash(self, user, hash): + """ + semi-private helper which allows writing a hash directly; + adds user if needed. + + .. warning:: + does not (currently) do any validation of the hash string + + .. versionadded:: 1.7 + """ + # assert self.context.identify(hash), "unrecognized hash format" + if PY3 and isinstance(hash, str): + hash = hash.encode(self.encoding) + user = self._encode_user(user) + existing = self._set_record(user, hash) + self._autosave() + return existing + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="get_hash") + def find(self, user): + """return hash for user""" + return self.get_hash(user) + + # XXX: rename to something more explicit, like delete_user()? + def delete(self, user): + """Delete user's entry. + + :returns: + * ``True`` if user deleted. + * ``False`` if user not found. + """ + try: + del self._records[self._encode_user(user)] + except KeyError: + return False + self._autosave() + return True + + def check_password(self, user, password): + """ + Verify password for specified user. + If algorithm marked as deprecated by CryptContext, will automatically be re-hashed. + + :returns: + * ``None`` if user not found. + * ``False`` if user found, but password does not match. + * ``True`` if user found and password matches. + + .. versionchanged:: 1.6 + This method was previously called ``verify``, it was renamed + to prevent ambiguity with the :class:`!CryptContext` method. + The old alias is deprecated, and will be removed in Passlib 1.8. + """ + user = self._encode_user(user) + hash = self._records.get(user) + if hash is None: + return None + if isinstance(password, unicode): + # NOTE: encoding password to match file, making the assumption + # that server will use same encoding to hash the password. + password = password.encode(self.encoding) + ok, new_hash = self.context.verify_and_update(password, hash) + if ok and new_hash is not None: + # rehash user's password if old hash was deprecated + assert user in self._records # otherwise would have to use ._set_record() + self._records[user] = new_hash + self._autosave() + return ok + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="check_password") + def verify(self, user, password): + """verify password for user""" + return self.check_password(user, password) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# htdigest editing +#============================================================================= +class HtdigestFile(_CommonFile): + """class for reading & writing Htdigest files. + + The class constructor accepts the following arguments: + + :type path: filepath + :param path: + + Specifies path to htdigest file, use to implicitly load from and save to. + + This class has two modes of operation: + + 1. It can be "bound" to a local file by passing a ``path`` to the class + constructor. In this case it will load the contents of the file when + created, and the :meth:`load` and :meth:`save` methods will automatically + load from and save to that file if they are called without arguments. + + 2. Alternately, it can exist as an independant object, in which case + :meth:`load` and :meth:`save` will require an explicit path to be + provided whenever they are called. As well, ``autosave`` behavior + will not be available. + + This feature is new in Passlib 1.6, and is the default if no + ``path`` value is provided to the constructor. + + This is also exposed as a readonly instance attribute. + + :type default_realm: str + :param default_realm: + + If ``default_realm`` is set, all the :class:`HtdigestFile` + methods that require a realm will use this value if one is not + provided explicitly. If unset, they will raise an error stating + that an explicit realm is required. + + This is also exposed as a writeable instance attribute. + + .. versionadded:: 1.6 + + :type new: bool + :param new: + + Normally, if *path* is specified, :class:`HtdigestFile` will + immediately load the contents of the file. However, when creating + a new htpasswd file, applications can set ``new=True`` so that + the existing file (if any) will not be loaded. + + .. versionadded:: 1.6 + This feature was previously enabled by setting ``autoload=False``. + That alias has been deprecated, and will be removed in Passlib 1.8 + + :type autosave: bool + :param autosave: + + Normally, any changes made to an :class:`HtdigestFile` instance + will not be saved until :meth:`save` is explicitly called. However, + if ``autosave=True`` is specified, any changes made will be + saved to disk immediately (assuming *path* has been set). + + This is also exposed as a writeable instance attribute. + + :type encoding: str + :param encoding: + + Optionally specify character encoding used to read/write file + and hash passwords. Defaults to ``utf-8``, though ``latin-1`` + is the only other commonly encountered encoding. + + This is also exposed as a readonly instance attribute. + + :param autoload: + Set to ``False`` to prevent the constructor from automatically + loaded the file from disk. + + .. deprecated:: 1.6 + This has been replaced by the *new* keyword. + Instead of setting ``autoload=False``, you should use + ``new=True``. Support for this keyword will be removed + in Passlib 1.8. + + Loading & Saving + ================ + .. automethod:: load + .. automethod:: load_if_changed + .. automethod:: load_string + .. automethod:: save + .. automethod:: to_string + + Inspection + ========== + .. automethod:: realms + .. automethod:: users + .. automethod:: check_password(user[, realm], password) + .. automethod:: get_hash + + Modification + ============ + .. automethod:: set_password(user[, realm], password) + .. automethod:: delete + .. automethod:: delete_realm + + Alternate Constructors + ====================== + .. automethod:: from_string + + Attributes + ========== + .. attribute:: default_realm + + The default realm that will be used if one is not provided + to methods that require it. By default this is ``None``, + in which case an explicit realm must be provided for every + method call. Can be written to. + + .. attribute:: path + + Path to local file that will be used as the default + for all :meth:`load` and :meth:`save` operations. + May be written to, initialized by the *path* constructor keyword. + + .. attribute:: autosave + + Writeable flag indicating whether changes will be automatically + written to *path*. + + Errors + ====== + :raises ValueError: + All of the methods in this class will raise a :exc:`ValueError` if + any user name or realm contains a forbidden character (one of ``:\\r\\n\\t\\x00``), + or is longer than 255 characters. + """ + #=================================================================== + # instance attrs + #=================================================================== + + # NOTE: _records map stores (,) for the key, + # and as the value, all as bytes. + + # NOTE: unlike htpasswd, this class doesn't use a CryptContext, + # as only one hash format is supported: htdigest. + + # optionally specify default realm that will be used if none + # is provided to a method call. otherwise realm is always required. + default_realm = None + + #=================================================================== + # init & serialization + #=================================================================== + def __init__(self, path=None, default_realm=None, **kwds): + self.default_realm = default_realm + super(HtdigestFile, self).__init__(path, **kwds) + + def _parse_record(self, record, lineno): + result = record.rstrip().split(_BCOLON) + if len(result) != 3: + raise ValueError("malformed htdigest file (error reading line %d)" + % lineno) + user, realm, hash = result + return (user, realm), hash + + def _render_record(self, key, hash): + user, realm = key + return render_bytes("%s:%s:%s\n", user, realm, hash) + + def _require_realm(self, realm): + if realm is None: + realm = self.default_realm + if realm is None: + raise TypeError("you must specify a realm explicitly, " + "or set the default_realm attribute") + return realm + + def _encode_realm(self, realm): + realm = self._require_realm(realm) + return self._encode_field(realm, "realm") + + def _encode_key(self, user, realm): + return self._encode_user(user), self._encode_realm(realm) + + #=================================================================== + # public methods + #=================================================================== + + def realms(self): + """Return list of all realms in database""" + realms = set(key[1] for key in self._records) + return [self._decode_field(realm) for realm in realms] + + def users(self, realm=None): + """Return list of all users in specified realm. + + * uses ``self.default_realm`` if no realm explicitly provided. + * returns empty list if realm not found. + """ + realm = self._encode_realm(realm) + return [self._decode_field(key[0]) for key in self._records + if key[1] == realm] + + ##def has_user(self, user, realm=None): + ## "check if user+realm combination exists" + ## return self._encode_key(user,realm) in self._records + + ##def rename_realm(self, old, new): + ## """rename all accounts in realm""" + ## old = self._encode_realm(old) + ## new = self._encode_realm(new) + ## keys = [key for key in self._records if key[1] == old] + ## for key in keys: + ## hash = self._records.pop(key) + ## self._set_record((key[0], new), hash) + ## self._autosave() + ## return len(keys) + + ##def rename(self, old, new, realm=None): + ## """rename user account""" + ## old = self._encode_user(old) + ## new = self._encode_user(new) + ## realm = self._encode_realm(realm) + ## hash = self._records.pop((old,realm)) + ## self._set_record((new, realm), hash) + ## self._autosave() + + def set_password(self, user, realm=None, password=_UNSET): + """Set password for user; adds user & realm if needed. + + If ``self.default_realm`` has been set, this may be called + with the syntax ``set_password(user, password)``, + otherwise it must be called with all three arguments: + ``set_password(user, realm, password)``. + + :returns: + * ``True`` if existing user was updated + * ``False`` if user account added. + """ + if password is _UNSET: + # called w/ two args - (user, password), use default realm + realm, password = None, realm + realm = self._require_realm(realm) + hash = htdigest.hash(password, user, realm, encoding=self.encoding) + return self.set_hash(user, realm, hash) + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="set_password") + def update(self, user, realm, password): + """set password for user""" + return self.set_password(user, realm, password) + + def get_hash(self, user, realm=None): + """Return :class:`~passlib.hash.htdigest` hash stored for user. + + * uses ``self.default_realm`` if no realm explicitly provided. + * returns ``None`` if user or realm not found. + + .. versionchanged:: 1.6 + This method was previously named ``find``, it was renamed + for clarity. The old name is deprecated, and will be removed + in Passlib 1.8. + """ + key = self._encode_key(user, realm) + hash = self._records.get(key) + if hash is None: + return None + if PY3: + hash = hash.decode(self.encoding) + return hash + + def set_hash(self, user, realm=None, hash=_UNSET): + """ + semi-private helper which allows writing a hash directly; + adds user & realm if needed. + + If ``self.default_realm`` has been set, this may be called + with the syntax ``set_hash(user, hash)``, + otherwise it must be called with all three arguments: + ``set_hash(user, realm, hash)``. + + .. warning:: + does not (currently) do any validation of the hash string + + .. versionadded:: 1.7 + """ + if hash is _UNSET: + # called w/ two args - (user, hash), use default realm + realm, hash = None, realm + # assert htdigest.identify(hash), "unrecognized hash format" + if PY3 and isinstance(hash, str): + hash = hash.encode(self.encoding) + key = self._encode_key(user, realm) + existing = self._set_record(key, hash) + self._autosave() + return existing + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="get_hash") + def find(self, user, realm): + """return hash for user""" + return self.get_hash(user, realm) + + # XXX: rename to something more explicit, like delete_user()? + def delete(self, user, realm=None): + """Delete user's entry for specified realm. + + if realm is not specified, uses ``self.default_realm``. + + :returns: + * ``True`` if user deleted, + * ``False`` if user not found in realm. + """ + key = self._encode_key(user, realm) + try: + del self._records[key] + except KeyError: + return False + self._autosave() + return True + + def delete_realm(self, realm): + """Delete all users for specified realm. + + if realm is not specified, uses ``self.default_realm``. + + :returns: number of users deleted (0 if realm not found) + """ + realm = self._encode_realm(realm) + records = self._records + keys = [key for key in records if key[1] == realm] + for key in keys: + del records[key] + self._autosave() + return len(keys) + + def check_password(self, user, realm=None, password=_UNSET): + """Verify password for specified user + realm. + + If ``self.default_realm`` has been set, this may be called + with the syntax ``check_password(user, password)``, + otherwise it must be called with all three arguments: + ``check_password(user, realm, password)``. + + :returns: + * ``None`` if user or realm not found. + * ``False`` if user found, but password does not match. + * ``True`` if user found and password matches. + + .. versionchanged:: 1.6 + This method was previously called ``verify``, it was renamed + to prevent ambiguity with the :class:`!CryptContext` method. + The old alias is deprecated, and will be removed in Passlib 1.8. + """ + if password is _UNSET: + # called w/ two args - (user, password), use default realm + realm, password = None, realm + user = self._encode_user(user) + realm = self._encode_realm(realm) + hash = self._records.get((user,realm)) + if hash is None: + return None + return htdigest.verify(password, hash, user, realm, + encoding=self.encoding) + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="check_password") + def verify(self, user, realm, password): + """verify password for user""" + return self.check_password(user, realm, password) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/apps.py b/venv/Lib/site-packages/passlib/apps.py new file mode 100644 index 0000000..682bbff --- /dev/null +++ b/venv/Lib/site-packages/passlib/apps.py @@ -0,0 +1,245 @@ +"""passlib.apps""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +from itertools import chain +# site +# pkg +from passlib import hash +from passlib.context import LazyCryptContext +from passlib.utils import sys_bits +# local +__all__ = [ + 'custom_app_context', + 'django_context', + 'ldap_context', 'ldap_nocrypt_context', + 'mysql_context', 'mysql4_context', 'mysql3_context', + 'phpass_context', + 'phpbb3_context', + 'postgres_context', +] + +#============================================================================= +# master containing all identifiable hashes +#============================================================================= +def _load_master_config(): + from passlib.registry import list_crypt_handlers + + # get master list + schemes = list_crypt_handlers() + + # exclude the ones we know have ambiguous or greedy identify() methods. + excluded = [ + # frequently confused for eachother + 'bigcrypt', + 'crypt16', + + # no good identifiers + 'cisco_pix', + 'cisco_type7', + 'htdigest', + 'mysql323', + 'oracle10', + + # all have same size + 'lmhash', + 'msdcc', + 'msdcc2', + 'nthash', + + # plaintext handlers + 'plaintext', + 'ldap_plaintext', + + # disabled handlers + 'django_disabled', + 'unix_disabled', + 'unix_fallback', + ] + for name in excluded: + schemes.remove(name) + + # return config + return dict(schemes=schemes, default="sha256_crypt") +master_context = LazyCryptContext(onload=_load_master_config) + +#============================================================================= +# for quickly bootstrapping new custom applications +#============================================================================= +custom_app_context = LazyCryptContext( + # choose some reasonbly strong schemes + schemes=["sha512_crypt", "sha256_crypt"], + + # set some useful global options + default="sha256_crypt" if sys_bits < 64 else "sha512_crypt", + + # set a good starting point for rounds selection + sha512_crypt__min_rounds = 535000, + sha256_crypt__min_rounds = 535000, + + # if the admin user category is selected, make a much stronger hash, + admin__sha512_crypt__min_rounds = 1024000, + admin__sha256_crypt__min_rounds = 1024000, + ) + +#============================================================================= +# django +#============================================================================= + +#----------------------------------------------------------------------- +# 1.0 +#----------------------------------------------------------------------- + +_django10_schemes = [ + "django_salted_sha1", + "django_salted_md5", + "django_des_crypt", + "hex_md5", + "django_disabled", +] + +django10_context = LazyCryptContext( + schemes=_django10_schemes, + default="django_salted_sha1", + deprecated=["hex_md5"], +) + +#----------------------------------------------------------------------- +# 1.4 +#----------------------------------------------------------------------- + +_django14_schemes = [ + "django_pbkdf2_sha256", + "django_pbkdf2_sha1", + "django_bcrypt" +] + _django10_schemes + +django14_context = LazyCryptContext( + schemes=_django14_schemes, + deprecated=_django10_schemes, +) + +#----------------------------------------------------------------------- +# 1.6 +#----------------------------------------------------------------------- + +_django16_schemes = list(_django14_schemes) +_django16_schemes.insert(1, "django_bcrypt_sha256") +django16_context = LazyCryptContext( + schemes=_django16_schemes, + deprecated=_django10_schemes, +) + +#----------------------------------------------------------------------- +# 1.10 +#----------------------------------------------------------------------- + +_django_110_schemes = [ + "django_pbkdf2_sha256", + "django_pbkdf2_sha1", + "django_argon2", + "django_bcrypt", + "django_bcrypt_sha256", + "django_disabled", +] +django110_context = LazyCryptContext(schemes=_django_110_schemes) + +#----------------------------------------------------------------------- +# 2.1 +#----------------------------------------------------------------------- + +_django21_schemes = list(_django_110_schemes) +_django21_schemes.remove("django_bcrypt") +django21_context = LazyCryptContext(schemes=_django21_schemes) + +#----------------------------------------------------------------------- +# latest +#----------------------------------------------------------------------- + +# this will always point to latest version in passlib +django_context = django21_context + +#============================================================================= +# ldap +#============================================================================= + +#: standard ldap schemes +std_ldap_schemes = [ + "ldap_salted_sha512", + "ldap_salted_sha256", + "ldap_salted_sha1", + "ldap_salted_md5", + "ldap_sha1", + "ldap_md5", + "ldap_plaintext", +] + +# create context with all std ldap schemes EXCEPT crypt +ldap_nocrypt_context = LazyCryptContext(std_ldap_schemes) + +# create context with all possible std ldap + ldap crypt schemes +def _iter_ldap_crypt_schemes(): + from passlib.utils import unix_crypt_schemes + return ('ldap_' + name for name in unix_crypt_schemes) + +def _iter_ldap_schemes(): + """helper which iterates over supported std ldap schemes""" + return chain(std_ldap_schemes, _iter_ldap_crypt_schemes()) +ldap_context = LazyCryptContext(_iter_ldap_schemes()) + +### create context with all std ldap schemes + crypt schemes for localhost +##def _iter_host_ldap_schemes(): +## "helper which iterates over supported std ldap schemes" +## from passlib.handlers.ldap_digests import get_host_ldap_crypt_schemes +## return chain(std_ldap_schemes, get_host_ldap_crypt_schemes()) +##ldap_host_context = LazyCryptContext(_iter_host_ldap_schemes()) + +#============================================================================= +# mysql +#============================================================================= +mysql3_context = LazyCryptContext(["mysql323"]) +mysql4_context = LazyCryptContext(["mysql41", "mysql323"], deprecated="mysql323") +mysql_context = mysql4_context # tracks latest mysql version supported + +#============================================================================= +# postgres +#============================================================================= +postgres_context = LazyCryptContext(["postgres_md5"]) + +#============================================================================= +# phpass & variants +#============================================================================= +def _create_phpass_policy(**kwds): + """helper to choose default alg based on bcrypt availability""" + kwds['default'] = 'bcrypt' if hash.bcrypt.has_backend() else 'phpass' + return kwds + +phpass_context = LazyCryptContext( + schemes=["bcrypt", "phpass", "bsdi_crypt"], + onload=_create_phpass_policy, + ) + +phpbb3_context = LazyCryptContext(["phpass"], phpass__ident="H") + +# TODO: support the drupal phpass variants (see phpass homepage) + +#============================================================================= +# roundup +#============================================================================= + +_std_roundup_schemes = [ "ldap_hex_sha1", "ldap_hex_md5", "ldap_des_crypt", "roundup_plaintext" ] +roundup10_context = LazyCryptContext(_std_roundup_schemes) + +# NOTE: 'roundup15' really applies to roundup 1.4.17+ +roundup_context = roundup15_context = LazyCryptContext( + schemes=_std_roundup_schemes + [ "ldap_pbkdf2_sha1" ], + deprecated=_std_roundup_schemes, + default = "ldap_pbkdf2_sha1", + ldap_pbkdf2_sha1__default_rounds = 10000, + ) + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/context.py b/venv/Lib/site-packages/passlib/context.py new file mode 100644 index 0000000..bc3cbf5 --- /dev/null +++ b/venv/Lib/site-packages/passlib/context.py @@ -0,0 +1,2637 @@ +"""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 import exc +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 "__all__" 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 exc.UnknownHashError("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 "" % 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 **source** is bytes. + Defaults to ``"utf-8"``. Ignored when loading from a dictionary. + + .. deprecated:: 1.8 + + This keyword, and support for bytes input, will be dropped in Passlib 2.0 + + :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, + "") + 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 ` + 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 ` 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 `. + If specified, this will return the catgory-specific default scheme instead. + + :returns: + name of the default scheme. + + .. seealso:: the :ref:`default ` 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 ` + 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 ` + 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 ` 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 `. + 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 ` 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 `. + 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 ` 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 `. + 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() ` 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() ` 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 ` 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 ` 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() ` 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() ` 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 ` 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 `. + 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 `. + + :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 +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/exc.py b/venv/Lib/site-packages/passlib/exc.py new file mode 100644 index 0000000..755c7dc --- /dev/null +++ b/venv/Lib/site-packages/passlib/exc.py @@ -0,0 +1,397 @@ +"""passlib.exc -- exceptions & warnings raised by passlib""" +#============================================================================= +# exceptions +#============================================================================= +class UnknownBackendError(ValueError): + """ + Error raised if multi-backend handler doesn't recognize backend name. + Inherits from :exc:`ValueError`. + + .. versionadded:: 1.7 + """ + def __init__(self, hasher, backend): + self.hasher = hasher + self.backend = backend + message = "%s: unknown backend: %r" % (hasher.name, backend) + ValueError.__init__(self, message) + + +# XXX: add a PasslibRuntimeError as base for Missing/Internal/Security runtime errors? + + +class MissingBackendError(RuntimeError): + """Error raised if multi-backend handler has no available backends; + or if specifically requested backend is not available. + + :exc:`!MissingBackendError` derives + from :exc:`RuntimeError`, since it usually indicates + lack of an external library or OS feature. + This is primarily raised by handlers which depend on + external libraries (which is currently just + :class:`~passlib.hash.bcrypt`). + """ + + +class InternalBackendError(RuntimeError): + """ + Error raised if something unrecoverable goes wrong with backend call; + such as if ``crypt.crypt()`` returning a malformed hash. + + .. versionadded:: 1.7.3 + """ + + +class PasswordValueError(ValueError): + """ + Error raised if a password can't be hashed / verified for various reasons. + This exception derives from the builtin :exc:`!ValueError`. + + May be thrown directly when password violates internal invariants of hasher + (e.g. some don't support NULL characters). Hashers may also throw more specific subclasses, + such as :exc:`!PasswordSizeError`. + + .. versionadded:: 1.7.3 + """ + pass + + +class PasswordSizeError(PasswordValueError): + """ + Error raised if a password exceeds the maximum size allowed + by Passlib (by default, 4096 characters); or if password exceeds + a hash-specific size limitation. + + This exception derives from :exc:`PasswordValueError` (above). + + Many password hash algorithms take proportionately larger amounts of time and/or + memory depending on the size of the password provided. This could present + a potential denial of service (DOS) situation if a maliciously large + password is provided to an application. Because of this, Passlib enforces + a maximum size limit, but one which should be *much* larger + than any legitimate password. :exc:`PasswordSizeError` derives + from :exc:`!ValueError`. + + .. note:: + Applications wishing to use a different limit should set the + ``PASSLIB_MAX_PASSWORD_SIZE`` environmental variable before + Passlib is loaded. The value can be any large positive integer. + + .. attribute:: max_size + + indicates the maximum allowed size. + + .. versionadded:: 1.6 + """ + + max_size = None + + def __init__(self, max_size, msg=None): + self.max_size = max_size + if msg is None: + msg = "password exceeds maximum allowed size" + PasswordValueError.__init__(self, msg) + + # this also prevents a glibc crypt segfault issue, detailed here ... + # http://www.openwall.com/lists/oss-security/2011/11/15/1 + +class PasswordTruncateError(PasswordSizeError): + """ + Error raised if password would be truncated by hash. + This derives from :exc:`PasswordSizeError` (above). + + Hashers such as :class:`~passlib.hash.bcrypt` can be configured to raises + this error by setting ``truncate_error=True``. + + .. attribute:: max_size + + indicates the maximum allowed size. + + .. versionadded:: 1.7 + """ + + def __init__(self, cls, msg=None): + if msg is None: + msg = ("Password too long (%s truncates to %d characters)" % + (cls.name, cls.truncate_size)) + PasswordSizeError.__init__(self, cls.truncate_size, msg) + + +class PasslibSecurityError(RuntimeError): + """ + Error raised if critical security issue is detected + (e.g. an attempt is made to use a vulnerable version of a bcrypt backend). + + .. versionadded:: 1.6.3 + """ + + +class TokenError(ValueError): + """ + Base error raised by v:mod:`passlib.totp` when + a token can't be parsed / isn't valid / etc. + Derives from :exc:`!ValueError`. + + Usually one of the more specific subclasses below will be raised: + + * :class:`MalformedTokenError` -- invalid chars, too few digits + * :class:`InvalidTokenError` -- no match found + * :class:`UsedTokenError` -- match found, but token already used + + .. versionadded:: 1.7 + """ + + #: default message to use if none provided -- subclasses may fill this in + _default_message = 'Token not acceptable' + + def __init__(self, msg=None, *args, **kwds): + if msg is None: + msg = self._default_message + ValueError.__init__(self, msg, *args, **kwds) + + +class MalformedTokenError(TokenError): + """ + Error raised by :mod:`passlib.totp` when a token isn't formatted correctly + (contains invalid characters, wrong number of digits, etc) + """ + _default_message = "Unrecognized token" + + +class InvalidTokenError(TokenError): + """ + Error raised by :mod:`passlib.totp` when a token is formatted correctly, + but doesn't match any tokens within valid range. + """ + _default_message = "Token did not match" + + +class UsedTokenError(TokenError): + """ + Error raised by :mod:`passlib.totp` if a token is reused. + Derives from :exc:`TokenError`. + + .. autoattribute:: expire_time + + .. versionadded:: 1.7 + """ + _default_message = "Token has already been used, please wait for another." + + #: optional value indicating when current counter period will end, + #: and a new token can be generated. + expire_time = None + + def __init__(self, *args, **kwds): + self.expire_time = kwds.pop("expire_time", None) + TokenError.__init__(self, *args, **kwds) + + +class UnknownHashError(ValueError): + """ + Error raised by :class:`~passlib.crypto.lookup_hash` if hash name is not recognized. + This exception derives from :exc:`!ValueError`. + + As of version 1.7.3, this may also be raised if hash algorithm is known, + but has been disabled due to FIPS mode (message will include phrase "disabled for fips"). + + As of version 1.7.4, this may be raised if a :class:`~passlib.context.CryptContext` + is unable to identify the algorithm used by a password hash. + + .. versionadded:: 1.7 + + .. versionchanged: 1.7.3 + added 'message' argument. + + .. versionchanged:: 1.7.4 + altered call signature. + """ + def __init__(self, message=None, value=None): + self.value = value + if message is None: + message = "unknown hash algorithm: %r" % value + self.message = message + ValueError.__init__(self, message, value) + + def __str__(self): + return self.message + + +#============================================================================= +# warnings +#============================================================================= +class PasslibWarning(UserWarning): + """base class for Passlib's user warnings, + derives from the builtin :exc:`UserWarning`. + + .. versionadded:: 1.6 + """ + +# XXX: there's only one reference to this class, and it will go away in 2.0; +# so can probably remove this along with this / roll this into PasslibHashWarning. +class PasslibConfigWarning(PasslibWarning): + """Warning issued when non-fatal issue is found related to the configuration + of a :class:`~passlib.context.CryptContext` instance. + + This occurs primarily in one of two cases: + + * The CryptContext contains rounds limits which exceed the hard limits + imposed by the underlying algorithm. + * An explicit rounds value was provided which exceeds the limits + imposed by the CryptContext. + + In both of these cases, the code will perform correctly & securely; + but the warning is issued as a sign the configuration may need updating. + + .. versionadded:: 1.6 + """ + +class PasslibHashWarning(PasslibWarning): + """Warning issued when non-fatal issue is found with parameters + or hash string passed to a passlib hash class. + + This occurs primarily in one of two cases: + + * A rounds value or other setting was explicitly provided which + exceeded the handler's limits (and has been clamped + by the :ref:`relaxed` flag). + + * A malformed hash string was encountered which (while parsable) + should be re-encoded. + + .. versionadded:: 1.6 + """ + +class PasslibRuntimeWarning(PasslibWarning): + """Warning issued when something unexpected happens during runtime. + + The fact that it's a warning instead of an error means Passlib + was able to correct for the issue, but that it's anomalous enough + that the developers would love to hear under what conditions it occurred. + + .. versionadded:: 1.6 + """ + +class PasslibSecurityWarning(PasslibWarning): + """Special warning issued when Passlib encounters something + that might affect security. + + .. versionadded:: 1.6 + """ + +#============================================================================= +# error constructors +# +# note: these functions are used by the hashes in Passlib to raise common +# error messages. They are currently just functions which return ValueError, +# rather than subclasses of ValueError, since the specificity isn't needed +# yet; and who wants to import a bunch of error classes when catching +# ValueError will do? +#============================================================================= + +def _get_name(handler): + return handler.name if handler else "" + +#------------------------------------------------------------------------ +# generic helpers +#------------------------------------------------------------------------ +def type_name(value): + """return pretty-printed string containing name of value's type""" + cls = value.__class__ + if cls.__module__ and cls.__module__ not in ["__builtin__", "builtins"]: + return "%s.%s" % (cls.__module__, cls.__name__) + elif value is None: + return 'None' + else: + return cls.__name__ + +def ExpectedTypeError(value, expected, param): + """error message when param was supposed to be one type, but found another""" + # NOTE: value is never displayed, since it may sometimes be a password. + name = type_name(value) + return TypeError("%s must be %s, not %s" % (param, expected, name)) + +def ExpectedStringError(value, param): + """error message when param was supposed to be unicode or bytes""" + return ExpectedTypeError(value, "unicode or bytes", param) + +#------------------------------------------------------------------------ +# hash/verify parameter errors +#------------------------------------------------------------------------ +def MissingDigestError(handler=None): + """raised when verify() method gets passed config string instead of hash""" + name = _get_name(handler) + return ValueError("expected %s hash, got %s config string instead" % + (name, name)) + +def NullPasswordError(handler=None): + """raised by OS crypt() supporting hashes, which forbid NULLs in password""" + name = _get_name(handler) + return PasswordValueError("%s does not allow NULL bytes in password" % name) + +#------------------------------------------------------------------------ +# errors when parsing hashes +#------------------------------------------------------------------------ +def InvalidHashError(handler=None): + """error raised if unrecognized hash provided to handler""" + return ValueError("not a valid %s hash" % _get_name(handler)) + +def MalformedHashError(handler=None, reason=None): + """error raised if recognized-but-malformed hash provided to handler""" + text = "malformed %s hash" % _get_name(handler) + if reason: + text = "%s (%s)" % (text, reason) + return ValueError(text) + +def ZeroPaddedRoundsError(handler=None): + """error raised if hash was recognized but contained zero-padded rounds field""" + return MalformedHashError(handler, "zero-padded rounds") + +#------------------------------------------------------------------------ +# settings / hash component errors +#------------------------------------------------------------------------ +def ChecksumSizeError(handler, raw=False): + """error raised if hash was recognized, but checksum was wrong size""" + # TODO: if handler.use_defaults is set, this came from app-provided value, + # not from parsing a hash string, might want different error msg. + checksum_size = handler.checksum_size + unit = "bytes" if raw else "chars" + reason = "checksum must be exactly %d %s" % (checksum_size, unit) + return MalformedHashError(handler, reason) + +#============================================================================= +# sensitive info helpers +#============================================================================= + +#: global flag, set temporarily by UTs to allow debug_only_repr() to display sensitive values. +ENABLE_DEBUG_ONLY_REPR = False + + +def debug_only_repr(value, param="hash"): + """ + helper used to display sensitive data (hashes etc) within error messages. + currently returns placeholder test UNLESS unittests are running, + in which case the real value is displayed. + + mainly useful to prevent hashes / secrets from being exposed in production tracebacks; + while still being visible from test failures. + + NOTE: api subject to change, may formalize this more in the future. + """ + if ENABLE_DEBUG_ONLY_REPR or value is None or isinstance(value, bool): + return repr(value) + return "<%s %s value omitted>" % (param, type(value)) + + +def CryptBackendError(handler, config, hash, # * + source="crypt.crypt()"): + """ + helper to generate standard message when ``crypt.crypt()`` returns invalid result. + takes care of automatically masking contents of config & hash outside of UTs. + """ + name = _get_name(handler) + msg = "%s returned invalid %s hash: config=%s hash=%s" % \ + (source, name, debug_only_repr(config), debug_only_repr(hash)) + raise InternalBackendError(msg) + +#============================================================================= +# eof +#=============================================================================