Mercurial > moin > 1.9
changeset 5919:efd7c0be3339
added passlib 1.6.1 to MoinMoin/support/
removed passlib's unit tests (so our test runner does not run them also)
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/__init__.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,3 @@ +"""passlib - suite of password hashing & generation routinges""" + +__version__ = '1.6.1'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/_setup/__init__.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,1 @@ +"""passlib.setup - helpers used by passlib's setup.py script"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/_setup/docdist.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,87 @@ +"custom command to build doc.zip file" +#============================================================================= +# imports +#============================================================================= +# core +import os +from distutils import dir_util +from distutils.cmd import Command +from distutils.errors import * +from distutils.spawn import spawn +# local +__all__ = [ + "docdist" +] +#============================================================================= +# command +#============================================================================= +class docdist(Command): + + description = "create zip file containing standalone html docs" + + user_options = [ + ('build-dir=', None, 'Build directory'), + ('dist-dir=', 'd', + "directory to put the source distribution archive(s) in " + "[default: dist]"), + ('format=', 'f', + "archive format to create (tar, ztar, gztar, zip)"), + ('sign', 's', 'sign files using gpg'), + ('identity=', 'i', 'GPG identity used to sign files'), + ] + + def initialize_options(self): + self.build_dir = None + self.dist_dir = None + self.format = None + self.keep_temp = False + self.sign = False + self.identity = None + + def finalize_options(self): + if self.identity and not self.sign: + raise DistutilsOptionError( + "Must use --sign for --identity to have meaning" + ) + if self.build_dir is None: + cmd = self.get_finalized_command('build') + self.build_dir = os.path.join(cmd.build_base, 'docdist') + if not self.dist_dir: + self.dist_dir = "dist" + if not self.format: + self.format = "zip" + + def run(self): + # call build sphinx to build docs + self.run_command("build_sphinx") + cmd = self.get_finalized_command("build_sphinx") + source_dir = cmd.builder_target_dir + + # copy to directory with appropriate name + dist = self.distribution + arc_name = "%s-docs-%s" % (dist.get_name(), dist.get_version()) + tmp_dir = os.path.join(self.build_dir, arc_name) + if os.path.exists(tmp_dir): + dir_util.remove_tree(tmp_dir, dry_run=self.dry_run) + self.copy_tree(source_dir, tmp_dir, preserve_symlinks=True) + + # make archive from dir + arc_base = os.path.join(self.dist_dir, arc_name) + self.arc_filename = self.make_archive(arc_base, self.format, + self.build_dir) + + # Sign if requested + if self.sign: + gpg_args = ["gpg", "--detach-sign", "-a", self.arc_filename] + if self.identity: + gpg_args[2:2] = ["--local-user", self.identity] + spawn(gpg_args, + dry_run=self.dry_run) + + # cleanup + if not self.keep_temp: + dir_util.remove_tree(tmp_dir, dry_run=self.dry_run) + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/_setup/stamp.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,57 @@ +"update version string during build" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import os +import re +import time +from distutils.dist import Distribution +# pkg +# local +__all__ = [ + "stamp_source", + "stamp_distutils_output", +] +#============================================================================= +# helpers +#============================================================================= +def get_command_class(opts, name): + return opts['cmdclass'].get(name) or Distribution().get_command_class(name) + +def stamp_source(base_dir, version, dry_run=False): + "update version string in passlib dist" + path = os.path.join(base_dir, "passlib", "__init__.py") + with open(path) as fh: + input = fh.read() + output, count = re.subn('(?m)^__version__\s*=.*$', + '__version__ = ' + repr(version), + input) + assert count == 1, "failed to replace version string" + if not dry_run: + os.unlink(path) # sdist likes to use hardlinks + with open(path, "w") as fh: + fh.write(output) + +def stamp_distutils_output(opts, version): + + # subclass buildpy to update version string in source + _build_py = get_command_class(opts, "build_py") + class build_py(_build_py): + def build_packages(self): + _build_py.build_packages(self) + stamp_source(self.build_lib, version, self.dry_run) + opts['cmdclass']['build_py'] = build_py + + # subclass sdist to do same thing + _sdist = get_command_class(opts, "sdist") + class sdist(_sdist): + def make_release_tree(self, base_dir, files): + _sdist.make_release_tree(self, base_dir, files) + stamp_source(base_dir, version, self.dry_run) + opts['cmdclass']['sdist'] = sdist + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/apache.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,1037 @@ +"""passlib.apache - apache password support""" +# XXX: relocate this to passlib.ext.apache? +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +from hashlib import md5 +import logging; log = logging.getLogger(__name__) +import os +import sys +from warnings import warn +# site +# pkg +from passlib.context import CryptContext +from passlib.exc import ExpectedStringError +from passlib.hash import htdigest +from passlib.utils import consteq, render_bytes, to_bytes, deprecated_method, is_ascii_codec +from passlib.utils.compat import b, bytes, join_bytes, str_to_bascii, u, \ + unicode, BytesIO, iteritems, imap, PY3 +# local +__all__ = [ + 'HtpasswdFile', + 'HtdigestFile', +] + +#============================================================================= +# constants & support +#============================================================================= +_UNSET = object() + +_BCOLON = b(":") + +# byte values that aren't allowed in fields. +_INVALID_FIELD_CHARS = b(":\n\r\t\x00") + +#============================================================================= +# backport of OrderedDict for PY2.5 +#============================================================================= +try: + from collections import OrderedDict +except ImportError: + # Python 2.5 + class OrderedDict(dict): + """hacked OrderedDict replacement. + + NOTE: this doesn't provide a full OrderedDict implementation, + just the minimum needed by the Htpasswd internals. + """ + def __init__(self): + self._keys = [] + + def __iter__(self): + return iter(self._keys) + + def __setitem__(self, key, value): + if key not in self: + self._keys.append(key) + super(OrderedDict, self).__setitem__(key, value) + + def __delitem__(self, key): + super(OrderedDict, self).__delitem__(key) + self._keys.remove(key) + + def iteritems(self): + return ((key, self[key]) for key in self) + + # these aren't used or implemented, so disabling them for safety. + update = pop = popitem = clear = keys = iterkeys = None + +#============================================================================= +# 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 + + # ordered dict mapping key -> value for all records in database. + # (e.g. user => hash for Htpasswd) + _records = 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 = OrderedDict() + + 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. + def _get_path(self): + return self._path + def _set_path(self, value): + if value != self._path: + self._mtime = 0 + self._path = value + path = property(_get_path, _set_path) + + @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" + # XXX: found reference that "#" comment lines may be supported by + # htpasswd, should verify this, and figure out how to handle them. + # if true, this would also affect what can be stored in user field. + # XXX: if multiple entries for a key, should we use the first one + # or the last one? going w/ first entry for now. + # XXX: how should this behave if parsing fails? currently + # it will contain everything that was loaded up to error. + # could clear / restore old state instead. + parse = self._parse_record + records = self._records = OrderedDict() + for idx, line in enumerate(lines): + key, value = parse(line, idx+1) + if key not in records: + records[key] = value + + def _parse_record(cls, record, lineno): # pragma: no cover - abstract method + "parse line of file into (key, value) pair" + raise NotImplementedError("should be implemented in subclass") + + #=================================================================== + # 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 _iter_lines(self): + "iterator yielding lines of database" + return (self._render_record(key,value) for key,value in iteritems(self._records)) + + def _render_record(cls, 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 editing +#============================================================================= + +# FIXME: apr_md5_crypt technically the default only for windows, netware and tpf. +# TODO: find out if htpasswd's "crypt" mode is a crypt() *call* or just des_crypt implementation. +# if the former, we can support anything supported by passlib.hosts.host_context, +# allowing more secure hashes than apr_md5_crypt to be used. +# could perhaps add this behavior as an option to the constructor. +# c.f. http://httpd.apache.org/docs/2.2/programs/htpasswd.html +htpasswd_context = CryptContext([ + "apr_md5_crypt", # man page notes supported everywhere, default on Windows, Netware, TPF + "des_crypt", # man page notes server does NOT support this on Windows, Netware, TPF + "ldap_sha1", # man page notes only for transitioning <-> ldap + "plaintext" # man page notes server ONLY supports this on Windows, Netware, TPF + ]) + +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. + Must be one of ``"apr_md5_crypt"``, ``"des_crypt"``, ``"ldap_sha1"``, + ``"plaintext"``. It defaults to ``"apr_md5_crypt"``. + + .. versionadded:: 1.6 + This keyword was previously named ``default``. That alias + has been deprecated, and will be removed in Passlib 1.8. + + :type context: :class:`~passlib.context.CryptContext` + :param context: + :class:`!CryptContext` instance used to encrypt + 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 usuable 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 encrypt 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 <user> for the key, and <hash> 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: + 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. + """ + user = self._encode_user(user) + hash = self.context.encrypt(password) + if PY3: + hash = hash.encode(self.encoding) + existing = (user in self._records) + self._records[user] = hash + self._autosave() + return existing + + @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 + + @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. + + :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 + 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 (<user>,<realm>) for the key, + # and <hash> as the value, all as <self.encoding> 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 _encode_realm(self, realm): + # override default _encode_realm to fill in default realm field + 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 self._encode_field(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" + ## user = self._encode_user(user) + ## realm = self._encode_realm(realm) + ## return (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._records[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._records[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 + user = self._encode_user(user) + realm = self._encode_realm(realm) + key = (user, realm) + existing = (key in self._records) + hash = htdigest.encrypt(password, user, realm, encoding=self.encoding) + if PY3: + hash = hash.encode(self.encoding) + self._records[key] = hash + self._autosave() + return existing + + @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) + + # XXX: rename to something more explicit, like get_hash()? + 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_user(user), self._encode_realm(realm)) + hash = self._records.get(key) + if hash is None: + return None + if PY3: + hash = hash.decode(self.encoding) + return hash + + @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_user(user), self._encode_realm(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 +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/apps.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,184 @@ +"""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", + all__vary_rounds = 0.1, + + # set a good starting point for rounds selection + sha512_crypt__min_rounds = 60000, + sha256_crypt__min_rounds = 80000, + + # if the admin user category is selected, make a much stronger hash, + admin__sha512_crypt__min_rounds = 120000, + admin__sha256_crypt__min_rounds = 160000, + ) + +#============================================================================= +# django +#============================================================================= +_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"], +) + +django14_context = LazyCryptContext( + schemes=["django_pbkdf2_sha256", "django_pbkdf2_sha1", "django_bcrypt"] \ + + _django10_schemes, + deprecated=_django10_schemes, +) + +# this will always point to latest version +django_context = django14_context + +#============================================================================= +# ldap +#============================================================================= +std_ldap_schemes = ["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 +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/context.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,2687 @@ +"""passlib.context - CryptContext implementation""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +from functools import update_wrapper +import inspect +import re +import hashlib +from math import log as logb, ceil +import logging; log = logging.getLogger(__name__) +import os +import re +from time import sleep +from warnings import warn +# site +# pkg +from passlib.exc import PasslibConfigWarning, ExpectedStringError, ExpectedTypeError +from passlib.registry import get_crypt_handler, _validate_handler_name +from passlib.utils import rng, tick, to_bytes, deprecated_method, \ + to_unicode, splitcomma +from passlib.utils.compat import bytes, iteritems, num_types, \ + PY2, PY3, PY_MIN_32, unicode, SafeConfigParser, \ + NativeStringIO, BytesIO, base_string_types +# local +__all__ = [ + 'CryptContext', + 'LazyCryptContext', + 'CryptPolicy', +] + +#============================================================================= +# support +#============================================================================= + +# private object to detect unset params +_UNSET = object() + +# TODO: merge the following helpers into _CryptConfig + +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. +_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 + +#============================================================================= +# 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 it's 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) + + 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)) + + 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) + 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 will be removed entirely in passlib 1.8 + """ + warn("get_min_verify_time() and min_verify_time option is deprecated, " + "and will be removed in Passlib 1.8", DeprecationWarning, + stacklevel=2) + return self._context._config.get_context_option_with_flag(category, "min_verify_time")[0] or 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._is_deprecated_scheme(name, category) + + #=================================================================== + # 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 + #=================================================================== + +#============================================================================= +# _CryptRecord helper class +#============================================================================= +class _CryptRecord(object): + """wraps a handler and automatically applies various options. + + this is a helper used internally by CryptContext in order to reduce the + amount of work that needs to be done by CryptContext.verify(). + this class takes in all the options for a particular (scheme, category) + combination, and attempts to provide as short a code-path as possible for + the particular configuration. + """ + + #=================================================================== + # instance attrs + #=================================================================== + + # informational attrs + handler = None # handler instance this is wrapping + category = None # user category this applies to + deprecated = False # set if handler itself has been deprecated in config + + # rounds management - filled in by _init_rounds_options() + _has_rounds_options = False # if _has_rounds_bounds OR _generate_rounds is set + _has_rounds_bounds = False # if either min_rounds or max_rounds set + _min_rounds = None # minimum rounds allowed by policy, or None + _max_rounds = None # maximum rounds allowed by policy, or None + _generate_rounds = None # rounds generation function, or None + + # encrypt()/genconfig() attrs + settings = None # options to be passed directly to encrypt() + + # verify() attrs + _min_verify_time = None + + # needs_update() attrs + _needs_update = None # optional callable provided by handler + _has_rounds_introspection = False # if rounds can be extract from hash + + # cloned directly from handler, not affected by config options. + identify = None + genhash = None + + #=================================================================== + # init + #=================================================================== + def __init__(self, handler, category=None, deprecated=False, + min_rounds=None, max_rounds=None, default_rounds=None, + vary_rounds=None, min_verify_time=None, + **settings): + # store basic bits + self.handler = handler + self.category = category + self.deprecated = deprecated + self.settings = settings + + # validate & normalize rounds options + self._init_rounds_options(min_rounds, max_rounds, default_rounds, + vary_rounds) + + # init wrappers for handler methods we modify args to + self._init_encrypt_and_genconfig() + self._init_verify(min_verify_time) + self._init_needs_update() + + # these aren't wrapped by _CryptRecord, copy them directly from handler. + self.identify = handler.identify + self.genhash = handler.genhash + + #=================================================================== + # virtual attrs + #=================================================================== + @property + def scheme(self): + return self.handler.name + + @property + def _errprefix(self): + "string used to identify record in error messages" + handler = self.handler + category = self.category + if category: + return "%s %s config" % (handler.name, category) + else: + return "%s config" % (handler.name,) + + def __repr__(self): # pragma: no cover -- debugging + return "<_CryptRecord 0x%x for %s>" % (id(self), self._errprefix) + + #=================================================================== + # rounds generation & limits - used by encrypt & deprecation code + #=================================================================== + def _init_rounds_options(self, mn, mx, df, vr): + "parse options and compile efficient generate_rounds function" + #---------------------------------------------------- + # extract hard limits from handler itself + #---------------------------------------------------- + handler = self.handler + if 'rounds' not in handler.setting_kwds: + # doesn't even support rounds keyword. + return + hmn = getattr(handler, "min_rounds", None) + hmx = getattr(handler, "max_rounds", None) + + def check_against_handler(value, name): + "issue warning if value outside handler limits" + if hmn is not None and value < hmn: + warn("%s: %s value is below handler minimum %d: %d" % + (self._errprefix, name, hmn, value), PasslibConfigWarning) + if hmx is not None and value > hmx: + warn("%s: %s value is above handler maximum %d: %d" % + (self._errprefix, name, hmx, value), PasslibConfigWarning) + + #---------------------------------------------------- + # set policy limits + #---------------------------------------------------- + if mn is not None: + if mn < 0: + raise ValueError("%s: min_rounds must be >= 0" % self._errprefix) + check_against_handler(mn, "min_rounds") + self._min_rounds = mn + self._has_rounds_bounds = True + + if mx is not None: + if mn is not None and mx < mn: + raise ValueError("%s: max_rounds must be " + ">= min_rounds" % self._errprefix) + elif mx < 0: + raise ValueError("%s: max_rounds must be >= 0" % self._errprefix) + check_against_handler(mx, "max_rounds") + self._max_rounds = mx + self._has_rounds_bounds = True + + #---------------------------------------------------- + # validate default_rounds + #---------------------------------------------------- + if df is not None: + if mn is not None and df < mn: + raise ValueError("%s: default_rounds must be " + ">= min_rounds" % self._errprefix) + if mx is not None and df > mx: + raise ValueError("%s: default_rounds must be " + "<= max_rounds" % self._errprefix) + check_against_handler(df, "default_rounds") + elif vr or mx or mn: + # need an explicit default to work with + df = getattr(handler, "default_rounds", None) or mx or mn + assert df is not None, "couldn't find fallback default_rounds" + else: + # no need for rounds generation + self._has_rounds_options = self._has_rounds_bounds + return + + # clip default to handler & policy limits *before* vary rounds + # is calculated, so that proportion vr values are scaled against + # the effective default. + def clip(value): + "clip value to intersection of policy + handler limits" + if mn is not None and value < mn: + value = mn + if hmn is not None and value < hmn: + value = hmn + if mx is not None and value > mx: + value = mx + if hmx is not None and value > hmx: + value = hmx + return value + df = clip(df) + + #---------------------------------------------------- + # validate vary_rounds, + # coerce df/vr to linear scale, + # and setup scale_value() to undo coercion + #---------------------------------------------------- + # NOTE: vr=0 same as if vr not set + if vr: + if vr < 0: + raise ValueError("%s: vary_rounds must be >= 0" % + self._errprefix) + def scale_value(value, upper): + return value + if isinstance(vr, float): + # vr is value from 0..1 expressing fraction of default rounds. + if vr > 1: + # XXX: deprecate 1.0 ? + raise ValueError("%s: vary_rounds must be < 1.0" % + self._errprefix) + # calculate absolute vr value based on df & rounds_cost + cost_scale = getattr(handler, "rounds_cost", "linear") + assert cost_scale in ["log2", "linear"] + if cost_scale == "log2": + # convert df & vr to linear scale for limit calc, + # and redefine scale_value() to convert back to log2. + df = 1<<df + def scale_value(value, upper): + if value <= 0: + return 0 + elif upper: + return int(logb(value,2)) + else: + return int(ceil(logb(value,2))) + vr = int(df*vr) + elif not isinstance(vr, int): + raise TypeError("vary_rounds must be int or float") + # else: vr is explicit number of rounds to vary df by. + + #---------------------------------------------------- + # set up rounds generation function. + #---------------------------------------------------- + if not vr: + # fixed rounds value + self._generate_rounds = lambda : df + else: + # randomly generate rounds in range df +/- vr + lower = clip(scale_value(df-vr,False)) + upper = clip(scale_value(df+vr,True)) + if lower == upper: + self._generate_rounds = lambda: upper + else: + assert lower < upper + self._generate_rounds = lambda: rng.randint(lower, upper) + + # hack for bsdi_crypt - want to avoid even-valued rounds + # NOTE: this technically might generate a rounds value 1 larger + # than the requested upper bound - but better to err on side of safety. + if getattr(handler, "_avoid_even_rounds", False): + gen = self._generate_rounds + self._generate_rounds = lambda : gen()|1 + + self._has_rounds_options = True + + #=================================================================== + # encrypt() / genconfig() + #=================================================================== + def _init_encrypt_and_genconfig(self): + "initialize genconfig/encrypt wrapper methods" + settings = self.settings + handler = self.handler + + # check no invalid settings are being set + keys = handler.setting_kwds + for key in settings: + if key not in keys: + raise KeyError("keyword not supported by %s handler: %r" % + (handler.name, key)) + + # if _prepare_settings() has nothing to do, bypass our wrappers + # with reference to original methods. + if not (settings or self._has_rounds_options): + self.genconfig = handler.genconfig + self.encrypt = handler.encrypt + + def genconfig(self, **kwds): + "wrapper for handler.genconfig() which adds custom settings/rounds" + self._prepare_settings(kwds) + return self.handler.genconfig(**kwds) + + def encrypt(self, secret, **kwds): + "wrapper for handler.encrypt() which adds custom settings/rounds" + self._prepare_settings(kwds) + return self.handler.encrypt(secret, **kwds) + + def _prepare_settings(self, kwds): + "add default values to settings for encrypt & genconfig" + # load in default values for any settings + if kwds: + for k,v in iteritems(self.settings): + if k not in kwds: + kwds[k] = v + else: + # faster, and the common case + kwds.update(self.settings) + + # handle rounds + if self._has_rounds_options: + rounds = kwds.get("rounds") + if rounds is None: + # fill in default rounds value + gen = self._generate_rounds + if gen: + kwds['rounds'] = gen() + elif self._has_rounds_bounds: + # check bounds for application-provided rounds value. + # XXX: should this raise an error instead of warning ? + # NOTE: stackdepth=4 is so that error matches + # where ctx.encrypt() was called by application code. + mn = self._min_rounds + if mn is not None and rounds < mn: + warn("%s requires rounds >= %d, increasing value from %d" % + (self._errprefix, mn, rounds), PasslibConfigWarning, 4) + rounds = mn + mx = self._max_rounds + if mx and rounds > mx: + warn("%s requires rounds <= %d, decreasing value from %d" % + (self._errprefix, mx, rounds), PasslibConfigWarning, 4) + rounds = mx + kwds['rounds'] = rounds + + #=================================================================== + # verify() + #=================================================================== + # TODO: once min_verify_time is removed, this will just be a clone + # of handler.verify() + + def _init_verify(self, mvt): + "initialize verify() wrapper - implements min_verify_time" + if mvt: + assert isinstance(mvt, (int,float)) and mvt > 0, "CryptPolicy should catch this" + self._min_verify_time = mvt + else: + # no mvt wrapper needed, so just use handler.verify directly + self.verify = self.handler.verify + + def verify(self, secret, hash, **context): + "verify helper - adds min_verify_time delay" + mvt = self._min_verify_time + assert mvt > 0, "wrapper should have been replaced for mvt=0" + start = tick() + if self.handler.verify(secret, hash, **context): + return True + end = tick() + delta = mvt + start - end + if delta > 0: + sleep(delta) + elif delta < 0: + # warn app they exceeded bounds (this might reveal + # relative costs of different hashes if under migration) + warn("CryptContext: verify exceeded min_verify_time: " + "scheme=%r min_verify_time=%r elapsed=%r" % + (self.scheme, mvt, end-start), PasslibConfigWarning) + return False + + #=================================================================== + # needs_update() + #=================================================================== + def _init_needs_update(self): + """initialize state for needs_update()""" + # if handler has been deprecated, replace wrapper and skip other checks + if self.deprecated: + self.needs_update = lambda hash, secret: True + return + + # let handler detect hashes with configurations that don't match + # current settings. currently do this by calling + # ``handler._bind_needs_update(**settings)``, which if defined + # should return None or a callable ``needs_update(hash,secret)->bool``. + # + # NOTE: this interface is still private, because it was hacked in + # for the sake of bcrypt & scram, and is subject to change. + handler = self.handler + const = getattr(handler, "_bind_needs_update", None) + if const: + self._needs_update = const(**self.settings) + + # XXX: what about a "min_salt_size" deprecator? + + # set flag if we can extract rounds from hash, allowing + # needs_update() to check for rounds that are outside of + # the configured range. + if self._has_rounds_bounds and hasattr(handler, "from_string"): + self._has_rounds_introspection = True + + def needs_update(self, hash, secret): + # init replaces this method entirely for this case. + ### check if handler has been deprecated + ##if self.deprecated: + ## return True + + # check handler's detector if it provided one. + check = self._needs_update + if check and check(hash, secret): + return True + + # XXX: should we use from_string() call below to check + # for config strings, and flag them as needing update? + # or throw an error? + # or leave that as an explicitly undefined border case, + # to keep the codepath simpler & faster? + + # if we can parse rounds parameter, check if it's w/in bounds. + if self._has_rounds_introspection: + # XXX: this might be a good place to use parsehash() + hash_obj = self.handler.from_string(hash) + try: + rounds = hash_obj.rounds + except AttributeError: # pragma: no cover -- sanity check + # XXX: all builtin hashes should have rounds attr, + # so should a warning be issues here? + pass + else: + mn = self._min_rounds + if mn is not None and rounds < mn: + return True + mx = self._max_rounds + if mx and rounds > mx: + return True + + return False + + #=================================================================== + # 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 it's 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 + + # dict mapping category -> default scheme + _default_schemes = None + + # dict mapping (scheme, category) -> _CryptRecord + _records = None + + # dict mapping category -> list of _CryptRecord 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, str): + 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, str): + 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) + if scheme: + # normalize scheme option + key, value = norm_scheme_option(key, value) + + # 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(key, value) + + # 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 == "rounds": + # for now, translating this to 'default_rounds' to be helpful. + # need to pick one of the two names as official, + # and deprecate the other one. + key = "default_rounds" + elif 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, str): + func = _coerce_scheme_options.get(key) + if func: + value = func(value) + return key, value + + def _norm_context_option(self, key, value): + schemes = self.schemes + if key == "default": + if hasattr(value, "name"): + value = value.name + elif not isinstance(value, str): + 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, str): + value = splitcomma(value) + elif not isinstance(value, (list,tuple)): + raise ExpectedTypeError(value, "str or seq", "deprecated") + if 'auto' in value: + 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, str): + 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' is deprecated as of Passlib 1.6, will be " + "ignored in 1.7, and removed in 1.8.", DeprecationWarning) + value = float(value) + if value < 0: + raise ValueError("'min_verify_time' must be >= 0") + 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. + (treat retval 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 + (treat return as readonly) + """ + try: + return self._scheme_options[scheme][category] + except KeyError: + return default + + 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)) + + # 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 violatiions 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 = {} + get_options = self._get_record_options_with_flag + categories = self.categories + for handler in self.handlers: + scheme = handler.name + kwds, _ = get_options(scheme, None) + records[scheme, None] = _CryptRecord(handler, **kwds) + for cat in categories: + kwds, has_cat_options = get_options(scheme, cat) + if has_cat_options: + records[scheme, cat] = _CryptRecord(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(). + + 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 it's output may eventually be made public. + + given a scheme & category, it returns two things: + a set of all the keyword options to pass to the _CryptRecord constructor, + 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* and *min_verify_time* keywords. + """ + # 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 + + # add in min_verify_time setting from context + value, not_inherited = self.get_context_option_with_flag(category, "min_verify_time") + if value: + kwds['min_verify_time'] = value + 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, str): + 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, str): + 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 _CryptRecord 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, base_string_types): + raise ExpectedStringError(hash, "hash") + # type check of category - handled by _get_record_list() + for record in self._get_record_list(category): + if record.identify(hash): + return record + if not required: + return None + elif not self.schemes: + raise KeyError("no crypt algorithms supported") + else: + raise ValueError("hash could not be identified") + + #=================================================================== + # 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 encrypting passwords using different 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. + + #=================================================================== + # 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, base_string_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" + + .. versionchanged:: 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 replace(self, **kwds): + "deprecated alias of :meth:`copy`" + warn("CryptContext().replace() has been deprecated in Passlib 1.6, " + "and will be removed in Passlib 1.8, " + "it has been renamed to CryptContext().copy()", + DeprecationWarning, stacklevel=2) + return self.copy(**kwds) + + #=================================================================== + # init + #=================================================================== + def __init__(self, schemes=None, + # keyword only... + policy=_UNSET, # <-- deprecated + _autoload=True, **kwds): + # XXX: add ability to make flag certain contexts as immutable, + # e.g. the builtin passlib ones? + # XXX: add a name or import path for the contexts, to help out repr? + if schemes is not None: + kwds['schemes'] = schemes + if policy is not _UNSET: + warn("The CryptContext ``policy`` keyword has been deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8; please use " + "``CryptContext.from_string()` or " + "``CryptContext.from_path()`` instead.", + DeprecationWarning) + if policy is None: + self.load(kwds) + elif isinstance(policy, CryptPolicy): + self.load(policy._context) + self.update(kwds) + else: + raise TypeError("policy must be a CryptPolicy instance") + elif _autoload: + self.load(kwds) + else: + assert not kwds, "_autoload=False and kwds are mutually exclusive" + + # XXX: would this be useful? + ##def __str__(self): + ## if PY3: + ## return self.to_string() + ## else: + ## return self.to_string().encode("utf-8") + + def __repr__(self): + return "<CryptContext at 0x%0x>" % id(self) + + #=================================================================== + # deprecated policy object + #=================================================================== + def _get_policy(self): + # The CryptPolicy class has been deprecated, so to support any + # legacy accesses, we create a stub policy object so .policy attr + # will continue to work. + # + # the code waits until app accesses a specific policy object attribute + # before issuing deprecation warning, so developer gets method-specific + # suggestion for how to upgrade. + + # NOTE: making a copy of the context so the policy acts like a snapshot, + # to retain the pre-1.6 behavior. + return CryptPolicy(_internal_context=self.copy(), _stub_policy=True) + + def _set_policy(self, policy): + warn("The CryptPolicy class and the ``context.policy`` attribute have " + "been deprecated as of Passlib 1.6, and will be removed in " + "Passlib 1.8; please use the ``context.load()`` and " + "``context.update()`` methods instead.", + DeprecationWarning, stacklevel=2) + if isinstance(policy, CryptPolicy): + self.load(policy._context) + else: + raise TypeError("expected CryptPolicy instance") + + policy = property(_get_policy, _set_policy, + doc="[deprecated] returns CryptPolicy instance " + "tied to this CryptContext") + + #=================================================================== + # loading / updating configuration + #=================================================================== + @staticmethod + def _parse_ini_stream(stream, section, filename): + "helper read INI from stream, extract passlib section as dict" + # NOTE: this expects a unicode stream under py3, + # and a utf-8 bytes stream under py2, + # allowing the resulting dict to always use native strings. + p = SafeConfigParser() + if PY_MIN_32: + # python 3.2 deprecated readfp in favor of read_file + p.read_file(stream, filename) + else: + p.readfp(stream, filename) + 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. It's 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 it's configuration + using :meth:`to_dict`. + + :type update: bool + :param update: + By default, :meth:`load` will replace the existing configuration + entirely. If ``update=True``, it will preserve any existing + configuration options that are not overridden by the new source, + much like the :meth:`update` method. + + :type section: str + :param section: + When parsing an INI-formatted string, :meth:`load` will look for + a section named ``"passlib"``. This option allows an alternate + section name to be used. Ignored when loading from a dictionary. + + :type encoding: str + :param encoding: + Encoding to use when decode bytes from string. + Defaults to ``"utf-8"``. Ignoring when loading from a dictionary. + + :raises TypeError: + * If the source cannot be identified. + * If an unknown / malformed keyword is encountered. + + :raises ValueError: + If an invalid keyword value is encountered. + + .. note:: + + If an error occurs during a :meth:`!load` call, the :class`!CryptContext` + instance will be restored to the configuration it was in before + the :meth:`!load` call was made; this is to ensure it is + *never* left in an inconsistent state due to a load error. + + .. versionadded:: 1.6 + """ + #----------------------------------------------------------- + # autodetect source type, convert to dict + #----------------------------------------------------------- + parse_keys = True + if isinstance(source, base_string_types): + if PY3: + source = to_unicode(source, encoding, param="source") + else: + source = to_bytes(source, "utf-8", source_encoding=encoding, + param="source") + source = self._parse_ini_stream(NativeStringIO(source), section, + "<string>") + 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, + # 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._get_record = config.get_record + self._identify_record = config.identify_record + + @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, str) + parts = ckey.replace(".","__").split("__") + count = len(parts) + if count == 1: + cat, scheme, key = None, None, parts[0] + elif count == 2: + cat = None + scheme, key = parts + elif count == 3: + cat, scheme, key = parts + else: + raise TypeError("keys must have less than 3 separators: %r" % + (ckey,)) + # validate & normalize the parts + if cat == "default": + cat = None + elif not cat and cat is not None: + raise TypeError("empty category: %r" % ckey) + if scheme == "context": + scheme = None + elif not scheme and scheme is not None: + raise TypeError("empty scheme: %r" % ckey) + if not key: + raise TypeError("empty option: %r" % ckey) + return cat, scheme, key + + def update(self, *args, **kwds): + """Helper for quickly changing configuration. + + This acts much like the :meth:`!dict.update` method: + it updates the context's configuration, + replacing the original value(s) for the specified keys, + and preserving the rest. + It accepts any :ref:`keyword <context-options>` + accepted by the :class:`!CryptContext` constructor. + + .. versionadded:: 1.6 + + .. seealso:: :meth:`copy` + """ + if args: + if len(args) > 1: + raise TypeError("expected at most one positional argument") + if kwds: + raise TypeError("positional arg and keywords mutually exclusive") + self.load(args[0], update=True) + elif kwds: + self.load(kwds, update=True) + + # XXX: make this public? even just as flag to load? + # FIXME: this function suffered some bitrot in 1.6.1, + # will need to be updated before works again. + ##def _simplify(self): + ## "helper to remove redundant/unused options" + ## # don't do anything if no schemes are defined + ## if not self._schemes: + ## return + ## + ## def strip_items(target, filter): + ## keys = [key for key,value in iteritems(target) + ## if filter(key,value)] + ## for key in keys: + ## del target[key] + ## + ## # remove redundant default. + ## defaults = self._default_schemes + ## if defaults.get(None) == self._schemes[0]: + ## del defaults[None] + ## + ## # remove options for unused schemes. + ## scheme_options = self._scheme_options + ## schemes = self._schemes + ("all",) + ## strip_items(scheme_options, lambda k,v: k not in schemes) + ## + ## # remove rendundant cat defaults. + ## cur = self.default_scheme() + ## strip_items(defaults, lambda k,v: k and v==cur) + ## + ## # remove redundant category deprecations. + ## # TODO: this should work w/ 'auto', but needs closer inspection + ## deprecated = self._deprecated_schemes + ## cur = self._deprecated_schemes.get(None) + ## strip_items(deprecated, lambda k,v: k and v==cur) + ## + ## # remove redundant category options. + ## for scheme, config in iteritems(scheme_options): + ## if None in config: + ## cur = config[None] + ## strip_items(config, lambda k,v: k and v==cur) + ## + ## # XXX: anything else? + + #=================================================================== + # reading configuration + #=================================================================== + def schemes(self, resolve=False): + """return schemes loaded into this CryptContext instance. + + :type resolve: bool + :arg resolve: + if ``True``, will return a tuple of :class:`~passlib.ifc.PasswordHash` + objects instead of their names. + + :returns: + returns tuple of the schemes configured for this context + via the *schemes* option. + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.schemes()`` + + .. seealso:: the :ref:`schemes <context-schemes-option>` option for usage example. + """ + return self._config.handlers if resolve else self._config.schemes + + # XXX: need to decide if exposing this would be useful to applications + # in any way that isn't already served by to_dict(); + # and then decide whether to expose ability as deprecated_schemes(), + # is_deprecated(), or a just add a schemes(deprecated=True) flag. + def _is_deprecated_scheme(self, scheme, category=None): + "helper used by unittests to check if scheme is deprecated" + return self._get_record(scheme, category).deprecated + + def default_scheme(self, category=None, resolve=False): + """return name of scheme that :meth:`encrypt` will use by default. + + :type resolve: bool + :arg resolve: + if ``True``, will return a :class:`~passlib.ifc.PasswordHash` + object instead of the name. + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>`. + If specified, this will return the catgory-specific default scheme instead. + + :returns: + name of the default scheme. + + .. seealso:: the :ref:`default <context-default-option>` option for usage example. + + .. versionadded:: 1.6 + """ + # type check of category - handled by _get_record() + record = self._get_record(None, category) + return record.handler if resolve else record.scheme + + # 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 + + def handler(self, scheme=None, category=None): + """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. + + :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()`` + """ + try: + return self._get_record(scheme, category).handler + 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)) + + #=================================================================== + # 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, str), \ + "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() + + #=================================================================== + # password hash api + #=================================================================== + + # NOTE: all the following methods do is look up the appropriate + # _CryptRecord for a given (scheme,category) combination, + # and hand off the real work to the record's methods, + # which are optimized for the specific (scheme,category) configuration. + # + # The record objects are cached inside the _CryptConfig + # instance stored in self._config, and are retreived + # 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, base_string_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 needs_update(self, hash, scheme=None, category=None, secret=None): + """Check if hash needs to be replaced for some reason, + in which case the secret should be re-hashed. + + This function is the core of CryptContext's support for hash migration: + This function takes in a hash string, and checks the scheme, + number of rounds, and other properties against the current policy. + It returns ``True`` if the hash is using a deprecated scheme, + or is otherwise outside of the bounds specified by the policy + (e.g. the number of rounds is lower than :ref:`min_rounds <context-min-rounds-option>` + configuration for that algorithm). + If so, the password should be re-encrypted using :meth:`encrypt` + Otherwise, it will return ``False``. + + :type hash: unicode or bytes + :arg hash: + The hash string to examine. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes <context-schemes-option>` option). + If no scheme is specified, it will be identified + based on the value of *hash*. + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>`. + If specified, this will cause any category-specific defaults to + be used when determining if the hash needs to be updated + (e.g. is below the minimum rounds). + + :type secret: unicode, bytes, or None + :param secret: + Optionally, the secret associated with the hash. + This is not required, or in fact useful for any current purpose, + and can be safely omitted. It's mainly present to allow the + development of future deprecation checks which might need this information. + + :returns: ``True`` if hash should be replaced, otherwise ``False``. + + .. versionchanged:: 1.6 + The *secret* argument was added, and this method was renamed + from the longer alias ``hash_needs_update``. + + .. seealso:: the :ref:`context-migration-example` example in the tutorial. + """ + record = self._get_or_identify_record(hash, scheme, category) + return record.needs_update(hash, secret) + + @deprecated_method(deprecated="1.6", removed="1.8", replacement="CryptContext.needs_update()") + def hash_needs_update(self, hash, scheme=None, category=None): + """legacy alias for :meth:`needs_update`""" + return self.needs_update(hash, scheme, category) + + def genconfig(self, scheme=None, category=None, **settings): + """Generate a config string for specified scheme. + + This wraps the :meth:`~passlib.ifc.PasswordHash.genconfig` + method of the appropriate algorithm, using the default if + one is not specified. + The main difference between this and calling a hash's + :meth:`!genconfig` method directly is that this way, the CryptContext + will add in any hash-specific options, such as the default rounds. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes <context-schemes-option>` option). + If no scheme is specified, the configured default + will be used. + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>`. + If specified, this will cause any category-specific defaults to + be used when hashing the password (e.g. different default scheme, + different default rounds values, etc). + + :param \*\*settings: + All additional keywords are passed to the appropriate handler, + and should match it's :attr:`~passlib.ifc.PasswordHash.setting_kwds`. + + :returns: + A configuration string suitable for passing to :meth:`~CryptContext.genhash`, + encoding all the provided settings and defaults; or ``None`` + if the selected algorithm doesn't support configuration strings. + The return value will always be a :class:`!str`. + """ + return self._get_record(scheme, category).genconfig(**settings) + + def genhash(self, secret, config, scheme=None, category=None, **kwds): + """Generate hash for the specified secret using another hash. + + This wraps the :meth:`~passlib.ifc.PasswordHash.genhash` + method of the appropriate algorithm, identifying it based + on the provided hash / configuration if a scheme is not specified + explicitly. + + :type secret: unicode or bytes + :arg secret: + the password to hash. + + :type config: unicode or bytes + :arg hash: + The hash or configuration string to extract the settings and salt + from when hashing the password. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes <context-schemes-option>` option). + If no scheme is specified, it will be identified + based on the value of *config*. + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>`. + Ignored by this function, this parameter + is provided for symmetry with the other methods. + + :param \*\*kwds: + All additional keywords are passed to the appropriate handler, + and should match it's :attr:`~passlib.ifc.PasswordHash.context_kwds`. + + :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. + * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.genhash` + method throws an error based on *secret* or the provided *kwds*. + """ + # XXX: could insert normalization to preferred unicode encoding here + return self._get_record(scheme, category).genhash(secret, config, **kwds) + + def identify(self, hash, category=None, resolve=False, required=False): + """Attempt to identify which algorithm the hash belongs to. + + Note that this will only consider the algorithms + currently configured for this context + (see the :ref:`schemes <context-schemes-option>` option). + All registered algorithms will be checked, from first to last, + and whichever one positively identifies the hash first will be returned. + + :type hash: unicode or bytes + :arg hash: + The hash string to test. + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>`. + Ignored by this function, this parameter + is provided for symmetry with the other methods. + + :type resolve: bool + :param resolve: + If ``True``, returns the hash handler itself, + instead of the name of the hash. + + :type required: bool + :param required: + If ``True``, this will raise a ValueError if the hash + cannot be identified, instead of returning ``None``. + + :returns: + The handler which first identifies the hash, + or ``None`` if none of the algorithms identify the hash. + """ + record = self._identify_record(hash, category, required) + if record is None: + return None + elif resolve: + return record.handler + else: + return record.scheme + + def encrypt(self, secret, scheme=None, category=None, **kwds): + """run secret through selected algorithm, returning resulting hash. + + :type secret: unicode or bytes + :arg secret: + the password to hash. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes <context-schemes-option>` option). + If no scheme is specified, the configured default + will be used. + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>`. + If specified, this will cause any category-specific defaults to + be used when hashing the password (e.g. different default scheme, + different default rounds values, etc). + + :param \*\*kwds: + All other keyword options are passed to the selected algorithm's + :meth:`~passlib.ifc.PasswordHash.encrypt` 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. + * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.encrypt` + method throws an error based on *secret* or the provided *kwds*. + + .. seealso:: the :ref:`context-basic-example` example in the tutorial + """ + # XXX: could insert normalization to preferred unicode encoding here + return self._get_record(scheme, category).encrypt(secret, **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 secret: unicode or bytes + :arg hash: + hash string to compare to + + :type scheme: str + :param scheme: + Optionally force context to use specific scheme. + This is usually not needed, as most hashes can be unambiguously + identified. Scheme must be one of the ones configured + for this context + (see the :ref:`schemes <context-schemes-option>` option). + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>` string. + This is mainly used when generating new hashes, it has little + effect when verifying; this keyword is mainly provided for symmetry. + + :param \*\*kwds: + All additional keywords are passed to the appropriate handler, + and should match it's :attr:`~passlib.ifc.PasswordHash.context_kwds`. + + :returns: + ``True`` if the password matched hash, else ``False``. + + :raises TypeError, ValueError: + * if any of the arguments have an invalid type or value. + * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.verify` + method throws an error based on *secret* or the provided *kwds*. + + :raises ValueError: if the hash could not be identified. + + .. seealso:: the :ref:`context-basic-example` example in the tutorial + """ + # XXX: have record strip context kwds if scheme doesn't use them? + # XXX: could insert normalization to preferred unicode encoding here + # XXX: what about supporting a setter() callback ala django 1.4 ? + record = self._get_or_identify_record(hash, scheme, category) + 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.encrypt`), 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 + + :type scheme: str + :param scheme: + Optionally force context to use specific scheme. + This is usually not needed, as most hashes can be unambiguously + identified. Scheme must be one of the ones configured + for this context + (see the :ref:`schemes <context-schemes-option>` option). + + :type category: str or None + :param category: + Optional :ref:`user category <user-categories>`. + If specified, this will cause any category-specific defaults to + be used if the password has to be re-hashed. + + :param \*\*kwds: + all additional keywords are passed to the appropriate handler, + and should match it's :attr:`context keywords <passlib.hash.PasswordHash.context_kwds>`. + + :returns: + This function returns a tuple containing two elements: + the first indicates whether the password verified, + and the second whether the existing hash needs to be replaced. + The return value 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 upgrading. + * ``(True, str)`` indicates the secret verified correctly, + and the existing hash needs to be updated. the :class:`!str` + will be the freshly generated hash to replace the old one with. + + .. seealso:: the :ref:`context-migration-example` example in the tutorial. + """ + # XXX: have record strip context kwds if scheme doesn't use them? + # XXX: could insert normalization to preferred unicode encoding here. + record = self._get_or_identify_record(hash, scheme, category) + if not record.verify(secret, hash, **kwds): + return False, None + elif record.needs_update(hash, secret): + # NOTE: we re-encrypt with default scheme, not current one. + return True, self.encrypt(secret, None, category, **kwds) + else: + return True, None + + #=================================================================== + # 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 load any handlers + (or even parse it's arguments) until + the first time one of it's 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. + """ + _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 +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/exc.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,175 @@ +"""passlib.exc -- exceptions & warnings raised by passlib""" +#============================================================================= +# exceptions +#============================================================================= +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 PasswordSizeError(ValueError): + """Error raised if a password exceeds the maximum size allowed + by Passlib (4096 characters). + + 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. + + .. versionadded:: 1.6 + """ + def __init__(self): + ValueError.__init__(self, "password exceeds maximum allowed size") + + # this also prevents a glibc crypt segfault issue, detailed here ... + # http://www.openwall.com/lists/oss-security/2011/11/15/1 + +#============================================================================= +# warnings +#============================================================================= +class PasslibWarning(UserWarning): + """base class for Passlib's user warnings. + + .. versionadded:: 1.6 + """ + +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. + """ + +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<relaxed-keyword>` flag). + + * A malformed hash string was encountered which (while parsable) + should be re-encoded. + """ + +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 anonmalous enough + that the developers would love to hear under what conditions it occurred. + """ + +class PasslibSecurityWarning(PasslibWarning): + """Special warning issued when Passlib encounters something + that might affect security. + """ + +#============================================================================= +# 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 "<unnamed>" + +#------------------------------------------------------------------------ +# 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) + +#------------------------------------------------------------------------ +# encrypt/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 ValueError("%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) + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/ext/__init__.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,1 @@ +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/ext/django/__init__.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,6 @@ +"""passlib.ext.django.models -- monkeypatch django hashing framework + +this plugin monkeypatches django's hashing framework +so that it uses a passlib context object, allowing handling of arbitrary +hashes in Django databases. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/ext/django/models.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,285 @@ +"""passlib.ext.django.models -- monkeypatch django hashing framework""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +from django import VERSION +from django.conf import settings +# pkg +from passlib.context import CryptContext +from passlib.exc import ExpectedTypeError +from passlib.ext.django.utils import _PatchManager, hasher_to_passlib_name, \ + get_passlib_hasher, get_preset_config +from passlib.utils.compat import callable, unicode, bytes +# local +__all__ = ["password_context"] + +#============================================================================= +# global attrs +#============================================================================= + +# the context object which this patches contrib.auth to use for password hashing. +# configuration controlled by ``settings.PASSLIB_CONFIG``. +password_context = CryptContext() + +# function mapping User objects -> passlib user category. +# may be overridden via ``settings.PASSLIB_GET_CATEGORY``. +def _get_category(user): + """default get_category() implementation""" + if user.is_superuser: + return "superuser" + elif user.is_staff: + return "staff" + else: + return None + +# object used to track state of patches applied to django. +_manager = _PatchManager(log=logging.getLogger(__name__ + "._manager")) + +# patch status +_patched = False + +#============================================================================= +# applying & removing the patches +#============================================================================= +def _apply_patch(): + """monkeypatch django's password handling to use ``passlib_context``, + assumes the caller will configure the object. + """ + # + # setup constants + # + log.debug("preparing to monkeypatch 'django.contrib.auth' ...") + global _patched + assert not _patched, "monkeypatching already applied" + HASHERS_PATH = "django.contrib.auth.hashers" + MODELS_PATH = "django.contrib.auth.models" + USER_PATH = MODELS_PATH + ":User" + FORMS_PATH = "django.contrib.auth.forms" + + # + # import UNUSUABLE_PASSWORD and is_password_usuable() helpers + # (providing stubs for older django versions) + # + if VERSION < (1,4): + has_hashers = False + if VERSION < (1,0): + UNUSABLE_PASSWORD = "!" + else: + from django.contrib.auth.models import UNUSABLE_PASSWORD + + def is_password_usable(encoded): + return (encoded is not None and encoded != UNUSABLE_PASSWORD) + + def is_valid_secret(secret): + return secret is not None + + else: + has_hashers = True + from django.contrib.auth.hashers import UNUSABLE_PASSWORD, \ + is_password_usable + + def is_valid_secret(secret): + # NOTE: changed in 1.4 - empty passwords no longer valid. + return bool(secret) + + # + # backport ``User.set_unusable_password()`` for Django 0.9 + # (simplifies rest of the code) + # + if not hasattr(_manager.getorig(USER_PATH), "set_unusable_password"): + assert VERSION < (1,0) + + @_manager.monkeypatch(USER_PATH) + def set_unusable_password(user): + user.password = UNUSABLE_PASSWORD + + @_manager.monkeypatch(USER_PATH) + def has_usable_password(user): + return is_password_usable(user.password) + + # + # patch ``User.set_password() & ``User.check_password()`` to use + # context & get_category (would just leave these as wrappers for hashers + # module under django 1.4, but then we couldn't pass User object into + # get_category very easily) + # + @_manager.monkeypatch(USER_PATH) + def set_password(user, password): + "passlib replacement for User.set_password()" + if is_valid_secret(password): + # NOTE: pulls _get_category from module globals + cat = _get_category(user) + user.password = password_context.encrypt(password, category=cat) + else: + user.set_unusable_password() + + @_manager.monkeypatch(USER_PATH) + def check_password(user, password): + "passlib replacement for User.check_password()" + hash = user.password + if not is_valid_secret(password) or not is_password_usable(hash): + return False + # NOTE: pulls _get_category from module globals + cat = _get_category(user) + ok, new_hash = password_context.verify_and_update(password, hash, + category=cat) + if ok and new_hash is not None: + # migrate to new hash if needed. + user.password = new_hash + user.save() + return ok + + # + # override check_password() with our own implementation + # + @_manager.monkeypatch(HASHERS_PATH, enable=has_hashers) + @_manager.monkeypatch(MODELS_PATH) + def check_password(password, encoded, setter=None, preferred="default"): + "passlib replacement for check_password()" + # XXX: this currently ignores "preferred" keyword, since it's purpose + # was for hash migration, and that's handled by the context. + if not is_valid_secret(password) or not is_password_usable(encoded): + return False + ok = password_context.verify(password, encoded) + if ok and setter and password_context.needs_update(encoded): + setter(password) + return ok + + # + # patch the other functions defined in the ``hashers`` module, as well + # as any other known locations where they're imported within ``contrib.auth`` + # + if has_hashers: + @_manager.monkeypatch(HASHERS_PATH) + @_manager.monkeypatch(MODELS_PATH) + def make_password(password, salt=None, hasher="default"): + "passlib replacement for make_password()" + if not is_valid_secret(password): + return UNUSABLE_PASSWORD + kwds = {} + if salt is not None: + kwds['salt'] = salt + if hasher != "default": + kwds['scheme'] = hasher_to_passlib_name(hasher) + return password_context.encrypt(password, **kwds) + + @_manager.monkeypatch(HASHERS_PATH) + @_manager.monkeypatch(FORMS_PATH) + def get_hasher(algorithm="default"): + "passlib replacement for get_hasher()" + if algorithm == "default": + scheme = None + else: + scheme = hasher_to_passlib_name(algorithm) + handler = password_context.handler(scheme) + return get_passlib_hasher(handler) + + # NOTE: custom helper that doesn't exist in django proper + # (though submitted a patch - https://code.djangoproject.com/ticket/18184) + @_manager.monkeypatch(HASHERS_PATH) + @_manager.monkeypatch(FORMS_PATH) + def identify_hasher(encoded): + "passlib helper to identify hasher from encoded password" + handler = password_context.identify(encoded, resolve=True, + required=True) + return get_passlib_hasher(handler) + + _patched = True + log.debug("... finished monkeypatching django") + +def _remove_patch(): + """undo the django monkeypatching done by this module. + offered as a last resort if it's ever needed. + + .. warning:: + This may cause problems if any other Django modules have imported + their own copies of the patched functions, though the patched + code has been designed to throw an error as soon as possible in + this case. + """ + global _patched + if _patched: + log.debug("removing django monkeypatching...") + _manager.unpatch_all(unpatch_conflicts=True) + password_context.load({}) + _patched = False + log.debug("...finished removing django monkeypatching") + return True + if _manager: # pragma: no cover -- sanity check + log.warning("reverting partial monkeypatching of django...") + _manager.unpatch_all() + password_context.load({}) + log.debug("...finished removing django monkeypatching") + return True + log.debug("django not monkeypatched") + return False + +#============================================================================= +# main code +#============================================================================= +def _load(): + global _get_category + + # TODO: would like to add support for inheriting config from a preset + # (or from existing hasher state) and letting PASSLIB_CONFIG + # be an update, not a replacement. + + # TODO: wrap and import any custom hashers as passlib handlers, + # so they could be used in the passlib config. + + # load config from settings + _UNSET = object() + config = getattr(settings, "PASSLIB_CONFIG", _UNSET) + if config is _UNSET: + # XXX: should probably deprecate this alias + config = getattr(settings, "PASSLIB_CONTEXT", _UNSET) + if config is _UNSET: + config = "passlib-default" + if config is None: + warn("setting PASSLIB_CONFIG=None is deprecated, " + "and support will be removed in Passlib 1.8, " + "use PASSLIB_CONFIG='disabled' instead.", + DeprecationWarning) + config = "disabled" + elif not isinstance(config, (unicode, bytes, dict)): + raise ExpectedTypeError(config, "str or dict", "PASSLIB_CONFIG") + + # load custom category func (if any) + get_category = getattr(settings, "PASSLIB_GET_CATEGORY", None) + if get_category and not callable(get_category): + raise ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY") + + # check if we've been disabled + if config == "disabled": + if _patched: # pragma: no cover -- sanity check + log.error("didn't expect monkeypatching would be applied!") + _remove_patch() + return + + # resolve any preset aliases + if isinstance(config, str) and '\n' not in config: + config = get_preset_config(config) + + # setup context + _apply_patch() + password_context.load(config) + if get_category: + # NOTE: _get_category is module global which is read by + # monkeypatched functions constructed by _apply_patch() + _get_category = get_category + log.debug("passlib.ext.django loaded") + +# wrap load function so we can undo any patching if something goes wrong +try: + _load() +except: + _remove_patch() + raise + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/ext/django/utils.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,470 @@ +"""passlib.ext.django.utils - helper functions used by this plugin""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +from weakref import WeakKeyDictionary +from warnings import warn +# site +try: + from django import VERSION as DJANGO_VERSION + log.debug("found django %r installation", DJANGO_VERSION) +except ImportError: + log.debug("django installation not found") + DJANGO_VERSION = () +# pkg +from passlib.context import CryptContext +from passlib.exc import PasslibRuntimeWarning +from passlib.registry import get_crypt_handler, list_crypt_handlers +from passlib.utils import classproperty +from passlib.utils.compat import bytes, get_method_function, iteritems +# local +__all__ = [ + "get_preset_config", + "get_passlib_hasher", +] + +#============================================================================= +# default policies +#============================================================================= +def get_preset_config(name): + """Returns configuration string for one of the preset strings + supported by the ``PASSLIB_CONFIG`` setting. + Currently supported presets: + + * ``"passlib-default"`` - default config used by this release of passlib. + * ``"django-default"`` - config matching currently installed django version. + * ``"django-latest"`` - config matching newest django version (currently same as ``"django-1.4"``). + * ``"django-1.0"`` - config used by stock Django 1.0 - 1.3 installs + * ``"django-1.4"`` -config used by stock Django 1.4 installs + """ + # TODO: add preset which includes HASHERS + PREFERRED_HASHERS, + # after having imported any custom hashers. "django-current" + if name == "django-default": + if (0,0) < DJANGO_VERSION < (1,4): + name = "django-1.0" + else: + name = "django-1.4" + if name == "django-1.0": + from passlib.apps import django10_context + return django10_context.to_string() + if name == "django-1.4" or name == "django-latest": + from passlib.apps import django14_context + return django14_context.to_string() + if name == "passlib-default": + return PASSLIB_DEFAULT + raise ValueError("unknown preset config name: %r" % name) + +# default context used by passlib 1.6 +PASSLIB_DEFAULT = """ +[passlib] + +; list of schemes supported by configuration +; currently all django 1.4 hashes, django 1.0 hashes, +; and three common modular crypt format hashes. +schemes = + django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, + django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5, + sha512_crypt, bcrypt, phpass + +; default scheme to use for new hashes +default = django_pbkdf2_sha256 + +; hashes using these schemes will automatically be re-hashed +; when the user logs in (currently all django 1.0 hashes) +deprecated = + django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, + django_des_crypt, hex_md5 + +; sets some common options, including minimum rounds for two primary hashes. +; if a hash has less than this number of rounds, it will be re-hashed. +all__vary_rounds = 0.05 +sha512_crypt__min_rounds = 80000 +django_pbkdf2_sha256__min_rounds = 10000 + +; set somewhat stronger iteration counts for ``User.is_staff`` +staff__sha512_crypt__default_rounds = 100000 +staff__django_pbkdf2_sha256__default_rounds = 12500 + +; and even stronger ones for ``User.is_superuser`` +superuser__sha512_crypt__default_rounds = 120000 +superuser__django_pbkdf2_sha256__default_rounds = 15000 +""" + +#============================================================================= +# translating passlib names <-> hasher names +#============================================================================= + +# prefix used to shoehorn passlib's handler names into django hasher namespace; +# allows get_hasher() to be meaningfully called even if passlib handler +# is the one being used. +PASSLIB_HASHER_PREFIX = "passlib_" + +# prefix all the django-specific hash formats are stored under w/in passlib; +# all of these hashes should expose their hasher name via ``.django_name``. +DJANGO_PASSLIB_PREFIX = "django_" + +# non-django-specific hashes which also expose ``.django_name``. +_other_django_hashes = ["hex_md5"] + +def passlib_to_hasher_name(passlib_name): + "convert passlib handler name -> hasher name" + handler = get_crypt_handler(passlib_name) + if hasattr(handler, "django_name"): + return handler.django_name + return PASSLIB_HASHER_PREFIX + passlib_name + +def hasher_to_passlib_name(hasher_name): + "convert hasher name -> passlib handler name" + if hasher_name.startswith(PASSLIB_HASHER_PREFIX): + return hasher_name[len(PASSLIB_HASHER_PREFIX):] + for name in list_crypt_handlers(): + if name.startswith(DJANGO_PASSLIB_PREFIX) or name in _other_django_hashes: + handler = get_crypt_handler(name) + if getattr(handler, "django_name", None) == hasher_name: + return name + # XXX: this should only happen for custom hashers that have been registered. + # _HasherHandler (below) is work in progress that would fix this. + raise ValueError("can't translate hasher name to passlib name: %r" % + hasher_name) + +#============================================================================= +# wrapping passlib handlers as django hashers +#============================================================================= +_FAKE_SALT = "--fake-salt--" + +class _HasherWrapper(object): + """helper for wrapping passlib handlers in Hasher-compatible class.""" + + # filled in by subclass, drives the other methods. + passlib_handler = None + + @classproperty + def algorithm(cls): + assert not hasattr(cls.passlib_handler, "django_name") + return PASSLIB_HASHER_PREFIX + cls.passlib_handler.name + + def salt(self): + # XXX: our encode wrapper generates a new salt each time it's called, + # so just returning a 'no value' flag here. + return _FAKE_SALT + + def verify(self, password, encoded): + return self.passlib_handler.verify(password, encoded) + + def encode(self, password, salt=None, iterations=None): + kwds = {} + if salt is not None and salt != _FAKE_SALT: + kwds['salt'] = salt + if iterations is not None: + kwds['rounds'] = iterations + return self.passlib_handler.encrypt(password, **kwds) + + _translate_kwds = dict(checksum="hash", rounds="iterations") + + def safe_summary(self, encoded): + from django.contrib.auth.hashers import mask_hash, _, SortedDict + handler = self.passlib_handler + items = [ + # since this is user-facing, we're reporting passlib's name, + # without the distracting PASSLIB_HASHER_PREFIX prepended. + (_('algorithm'), handler.name), + ] + if hasattr(handler, "parsehash"): + kwds = handler.parsehash(encoded, sanitize=mask_hash) + for key, value in iteritems(kwds): + key = self._translate_kwds.get(key, key) + items.append((_(key), value)) + return SortedDict(items) + +# cache of hasher wrappers generated by get_passlib_hasher() +_hasher_cache = WeakKeyDictionary() + +def get_passlib_hasher(handler): + """create *Hasher*-compatible wrapper for specified passlib hash. + + This takes in the name of a passlib hash (or the handler object itself), + and returns a wrapper instance which should be compatible with + Django 1.4's Hashers framework. + + If the named hash corresponds to one of Django's builtin hashers, + an instance of the real hasher class will be returned. + + Note that the format of the handler won't be altered, + so will probably not be compatible with Django's algorithm format, + so the monkeypatch provided by this plugin must have been applied. + + .. note:: + This function requires Django 1.4 or later. + """ + if DJANGO_VERSION < (1,4): + raise RuntimeError("get_passlib_hasher() requires Django >= 1.4") + if isinstance(handler, str): + handler = get_crypt_handler(handler) + if hasattr(handler, "django_name"): + # return native hasher instance + # XXX: should cache this too. + return _get_hasher(handler.django_name) + if handler.name == "django_disabled": + raise ValueError("can't wrap unusable-password handler") + try: + return _hasher_cache[handler] + except KeyError: + name = "Passlib_%s_PasswordHasher" % handler.name.title() + cls = type(name, (_HasherWrapper,), dict(passlib_handler=handler)) + hasher = _hasher_cache[handler] = cls() + return hasher + +def _get_hasher(algorithm): + "wrapper to call django.contrib.auth.hashers:get_hasher()" + import sys + module = sys.modules.get("passlib.ext.django.models") + if module is None: + # we haven't patched django, so just import directly + from django.contrib.auth.hashers import get_hasher + else: + # we've patched django, so have to use patch manager to retreive + # original get_hasher() function... + get_hasher = module._manager.getorig("django.contrib.auth.hashers:get_hasher") + return get_hasher(algorithm) + +#============================================================================= +# adapting django hashers -> passlib handlers +#============================================================================= +# TODO: this code probably halfway works, mainly just needs +# a routine to read HASHERS and PREFERRED_HASHER. + +##from passlib.registry import register_crypt_handler +##from passlib.utils import classproperty, to_native_str, to_unicode +##from passlib.utils.compat import unicode +## +## +##class _HasherHandler(object): +## "helper for wrapping Hasher instances as passlib handlers" +## # FIXME: this generic wrapper doesn't handle custom settings +## # FIXME: genconfig / genhash not supported. +## +## def __init__(self, hasher): +## self.django_hasher = hasher +## if hasattr(hasher, "iterations"): +## # assume encode() accepts an "iterations" parameter. +## # fake min/max rounds +## self.min_rounds = 1 +## self.max_rounds = 0xFFFFffff +## self.default_rounds = self.django_hasher.iterations +## self.setting_kwds += ("rounds",) +## +## # hasher instance - filled in by constructor +## django_hasher = None +## +## setting_kwds = ("salt",) +## context_kwds = () +## +## @property +## def name(self): +## # XXX: need to make sure this wont' collide w/ builtin django hashes. +## # maybe by renaming this to django compatible aliases? +## return DJANGO_PASSLIB_PREFIX + self.django_name +## +## @property +## def django_name(self): +## # expose this so hasher_to_passlib_name() extracts original name +## return self.django_hasher.algorithm +## +## @property +## def ident(self): +## # this should always be correct, as django relies on ident prefix. +## return unicode(self.django_name + "$") +## +## @property +## def identify(self, hash): +## # this should always work, as django relies on ident prefix. +## return to_unicode(hash, "latin-1", "hash").startswith(self.ident) +## +## @property +## def genconfig(self): +## # XXX: not sure how to support this. +## return None +## +## @property +## def genhash(self, secret, config): +## if config is not None: +## # XXX: not sure how to support this. +## raise NotImplementedError("genhash() for hashers not implemented") +## return self.encrypt(secret) +## +## @property +## def encrypt(self, secret, salt=None, **kwds): +## # NOTE: from how make_password() is coded, all hashers +## # should have salt param. but only some will have +## # 'iterations' parameter. +## opts = {} +## if 'rounds' in self.setting_kwds and 'rounds' in kwds: +## opts['iterations'] = kwds.pop("rounds") +## if kwds: +## raise TypeError("unexpected keyword arguments: %r" % list(kwds)) +## if isinstance(secret, unicode): +## secret = secret.encode("utf-8") +## if salt is None: +## salt = self.django_hasher.salt() +## return to_native_str(self.django_hasher(secret, salt, **opts)) +## +## @property +## def verify(self, secret, hash): +## hash = to_native_str(hash, "utf-8", "hash") +## if isinstance(secret, unicode): +## secret = secret.encode("utf-8") +## return self.django_hasher.verify(secret, hash) +## +##def register_hasher(hasher): +## handler = _HasherHandler(hasher) +## register_crypt_handler(handler) +## return handler + +#============================================================================= +# monkeypatch helpers +#============================================================================= +# private singleton indicating lack-of-value +_UNSET = object() + +class _PatchManager(object): + "helper to manage monkeypatches and run sanity checks" + + # NOTE: this could easily use a dict interface, + # but keeping it distinct to make clear that it's not a dict, + # since it has important side-effects. + + #=================================================================== + # init and support + #=================================================================== + def __init__(self, log=None): + # map of key -> (original value, patched value) + # original value may be _UNSET + self.log = log or logging.getLogger(__name__ + "._PatchManager") + self._state = {} + + # bool value tests if any patches are currently applied. + __bool__ = __nonzero__ = lambda self: bool(self._state) + + def _import_path(self, path): + "retrieve obj and final attribute name from resource path" + name, attr = path.split(":") + obj = __import__(name, fromlist=[attr], level=0) + while '.' in attr: + head, attr = attr.split(".", 1) + obj = getattr(obj, head) + return obj, attr + + @staticmethod + def _is_same_value(left, right): + "check if two values are the same (stripping method wrappers, etc)" + return get_method_function(left) == get_method_function(right) + + #=================================================================== + # reading + #=================================================================== + def _get_path(self, key, default=_UNSET): + obj, attr = self._import_path(key) + return getattr(obj, attr, default) + + def get(self, path, default=None): + "return current value for path" + return self._get_path(path, default) + + def getorig(self, path, default=None): + "return original (unpatched) value for path" + try: + value, _= self._state[path] + except KeyError: + value = self._get_path(path) + return default if value is _UNSET else value + + def check_all(self, strict=False): + """run sanity check on all keys, issue warning if out of sync""" + same = self._is_same_value + for path, (orig, expected) in iteritems(self._state): + if same(self._get_path(path), expected): + continue + msg = "another library has patched resource: %r" % path + if strict: + raise RuntimeError(msg) + else: + warn(msg, PasslibRuntimeWarning) + + #=================================================================== + # patching + #=================================================================== + def _set_path(self, path, value): + obj, attr = self._import_path(path) + if value is _UNSET: + if hasattr(obj, attr): + delattr(obj, attr) + else: + setattr(obj, attr, value) + + def patch(self, path, value): + "monkeypatch object+attr at <path> to have <value>, stores original" + assert value != _UNSET + current = self._get_path(path) + try: + orig, expected = self._state[path] + except KeyError: + self.log.debug("patching resource: %r", path) + orig = current + else: + self.log.debug("modifying resource: %r", path) + if not self._is_same_value(current, expected): + warn("overridding resource another library has patched: %r" + % path, PasslibRuntimeWarning) + self._set_path(path, value) + self._state[path] = (orig, value) + + ##def patch_many(self, **kwds): + ## "override specified resources with new values" + ## for path, value in iteritems(kwds): + ## self.patch(path, value) + + def monkeypatch(self, parent, name=None, enable=True): + "function decorator which patches function of same name in <parent>" + def builder(func): + if enable: + sep = "." if ":" in parent else ":" + path = parent + sep + (name or func.__name__) + self.patch(path, func) + return func + return builder + + #=================================================================== + # unpatching + #=================================================================== + def unpatch(self, path, unpatch_conflicts=True): + try: + orig, expected = self._state[path] + except KeyError: + return + current = self._get_path(path) + self.log.debug("unpatching resource: %r", path) + if not self._is_same_value(current, expected): + if unpatch_conflicts: + warn("reverting resource another library has patched: %r" + % path, PasslibRuntimeWarning) + else: + warn("not reverting resource another library has patched: %r" + % path, PasslibRuntimeWarning) + del self._state[path] + return + self._set_path(path, orig) + del self._state[path] + + def unpatch_all(self, **kwds): + for key in list(self._state): + self.unpatch(key, **kwds) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/__init__.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,1 @@ +"""passlib.handlers -- holds implementations of all passlib's builtin hash formats"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/bcrypt.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,335 @@ +"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm. + +TODO: + +* support 2x and altered-2a hashes? + http://www.openwall.com/lists/oss-security/2011/06/27/9 + +* deal with lack of PY3-compatibile c-ext implementation +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, absolute_import +# core +import os +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +try: + from bcrypt import hashpw as pybcrypt_hashpw +except ImportError: # pragma: no cover + pybcrypt_hashpw = None +try: + from bcryptor.engine import Engine as bcryptor_engine +except ImportError: # pragma: no cover + bcryptor_engine = None +# pkg +from passlib.exc import PasslibHashWarning +from passlib.utils import bcrypt64, safe_crypt, repeat_string, \ + classproperty, rng, getrandstr, test_crypt +from passlib.utils.compat import bytes, b, u, uascii_to_str, unicode, str_to_uascii +import passlib.utils.handlers as uh + +# local +__all__ = [ + "bcrypt", +] + +#============================================================================= +# support funcs & constants +#============================================================================= +_builtin_bcrypt = None + +def _load_builtin(): + global _builtin_bcrypt + if _builtin_bcrypt is None: + from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt + +IDENT_2 = u("$2$") +IDENT_2A = u("$2a$") +IDENT_2X = u("$2x$") +IDENT_2Y = u("$2y$") +_BNULL = b('\x00') + +#============================================================================= +# handler +#============================================================================= +class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler): + """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 12, must be between 4 and 31, inclusive. + This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}` + -- increasing the rounds by +1 will double the amount of time taken. + + :type ident: str + :param ident: + Specifies which version of the BCrypt algorithm will be used when creating a new hash. + Typically this option is not needed, as the default (``"2a"``) is usually the correct choice. + If specified, it must be one of the following: + + * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore. + * ``"2a"`` - latest revision of the official BCrypt algorithm, and the current default. + * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, + identical to ``"2a"`` in all but name. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This class now supports ``"2y"`` hashes, and recognizes + (but does not support) the broken ``"2x"`` hashes. + (see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>` + for details). + + .. versionchanged:: 1.6 + Added a pure-python backend. + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "bcrypt" + setting_kwds = ("salt", "rounds", "ident") + checksum_size = 31 + checksum_chars = bcrypt64.charmap + + #--HasManyIdents-- + default_ident = u("$2a$") + ident_values = (u("$2$"), IDENT_2A, IDENT_2X, IDENT_2Y) + ident_aliases = {u("2"): u("$2$"), u("2a"): IDENT_2A, u("2y"): IDENT_2Y} + + #--HasSalt-- + min_salt_size = max_salt_size = 22 + salt_chars = bcrypt64.charmap + # NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap + + #--HasRounds-- + default_rounds = 12 # current passlib default + min_rounds = 4 # bcrypt spec specified minimum + max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds) + rounds_cost = "log2" + + #=================================================================== + # formatting + #=================================================================== + + @classmethod + def from_string(cls, hash): + ident, tail = cls._parse_ident(hash) + if ident == IDENT_2X: + raise ValueError("crypt_blowfish's buggy '2x' hashes are not " + "currently supported") + rounds_str, data = tail.split(u("$")) + rounds = int(rounds_str) + if rounds_str != u('%02d') % (rounds,): + raise uh.exc.MalformedHashError(cls, "malformed cost field") + salt, chk = data[:22], data[22:] + return cls( + rounds=rounds, + salt=salt, + checksum=chk or None, + ident=ident, + ) + + def to_string(self): + hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, + self.checksum or u('')) + return uascii_to_str(hash) + + def _get_config(self, ident=None): + "internal helper to prepare config string for backends" + if ident is None: + ident = self.ident + if ident == IDENT_2Y: + ident = IDENT_2A + else: + assert ident != IDENT_2X + config = u("%s%02d$%s") % (ident, self.rounds, self.salt) + return uascii_to_str(config) + + #=================================================================== + # specialized salt generation - fixes passlib issue 25 + #=================================================================== + + @classmethod + def _bind_needs_update(cls, **settings): + return cls._needs_update + + @classmethod + def _needs_update(cls, hash, secret): + if isinstance(hash, bytes): + hash = hash.decode("ascii") + # check for incorrect padding bits (passlib issue 25) + if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]: + return True + # TODO: try to detect incorrect $2x$ hashes using *secret* + return False + + @classmethod + def normhash(cls, hash): + "helper to normalize hash, correcting any bcrypt padding bits" + if cls.identify(hash): + return cls.from_string(hash).to_string() + else: + return hash + + def _generate_salt(self, salt_size): + # override to correct generate salt bits + salt = super(bcrypt, self)._generate_salt(salt_size) + return bcrypt64.repair_unused(salt) + + def _norm_salt(self, salt, **kwds): + salt = super(bcrypt, self)._norm_salt(salt, **kwds) + assert salt is not None, "HasSalt didn't generate new salt!" + changed, salt = bcrypt64.check_repair_unused(salt) + if changed: + # FIXME: if salt was provided by user, this message won't be + # correct. not sure if we want to throw error, or use different warning. + warn( + "encountered a bcrypt salt with incorrectly set padding bits; " + "you may want to use bcrypt.normhash() " + "to fix this; see Passlib 1.5.3 changelog.", + PasslibHashWarning) + return salt + + def _norm_checksum(self, checksum): + checksum = super(bcrypt, self)._norm_checksum(checksum) + if not checksum: + return None + changed, checksum = bcrypt64.check_repair_unused(checksum) + if changed: + warn( + "encountered a bcrypt hash with incorrectly set padding bits; " + "you may want to use bcrypt.normhash() " + "to fix this; see Passlib 1.5.3 changelog.", + PasslibHashWarning) + return checksum + + #=================================================================== + # primary interface + #=================================================================== + backends = ("pybcrypt", "bcryptor", "os_crypt", "builtin") + + @classproperty + def _has_backend_pybcrypt(cls): + return pybcrypt_hashpw is not None + + @classproperty + def _has_backend_bcryptor(cls): + return bcryptor_engine is not None + + @classproperty + def _has_backend_builtin(cls): + if os.environ.get("PASSLIB_BUILTIN_BCRYPT") not in ["enable","enabled"]: + return False + # look at it cross-eyed, and it loads itself + _load_builtin() + return True + + @classproperty + def _has_backend_os_crypt(cls): + # XXX: what to do if only h2 is supported? h1 is *very* rare. + h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju' + h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty' + return test_crypt("test",h1) and test_crypt("test", h2) + + @classmethod + def _no_backends_msg(cls): + return "no bcrypt backends available - please install py-bcrypt" + + def _calc_checksum_os_crypt(self, secret): + config = self._get_config() + hash = safe_crypt(secret, config) + if hash: + assert hash.startswith(config) and len(hash) == len(config)+31 + return hash[-31:] + else: + # NOTE: it's unlikely any other backend will be available, + # but checking before we bail, just in case. + for name in self.backends: + if name != "os_crypt" and self.has_backend(name): + func = getattr(self, "_calc_checksum_" + name) + return func(secret) + raise uh.exc.MissingBackendError( + "password can't be handled by os_crypt, " + "recommend installing py-bcrypt.", + ) + + def _calc_checksum_pybcrypt(self, secret): + # py-bcrypt behavior: + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes taken as-is; returns ascii bytes. + # py3: not supported (patch submitted) + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if _BNULL in secret: + raise uh.exc.NullPasswordError(self) + config = self._get_config() + hash = pybcrypt_hashpw(secret, config) + assert hash.startswith(config) and len(hash) == len(config)+31 + return str_to_uascii(hash[-31:]) + + def _calc_checksum_bcryptor(self, secret): + # bcryptor behavior: + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes taken as-is; returns ascii bytes. + # py3: not supported + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if _BNULL in secret: + # NOTE: especially important to forbid NULLs for bcryptor, + # since it happily accepts them, and then silently truncates + # the password at first one it encounters :( + raise uh.exc.NullPasswordError(self) + if self.ident == IDENT_2: + # bcryptor doesn't support $2$ hashes; but we can fake $2$ behavior + # using the $2a$ algorithm, by repeating the password until + # it's at least 72 chars in length. + if secret: + secret = repeat_string(secret, 72) + config = self._get_config(IDENT_2A) + else: + config = self._get_config() + hash = bcryptor_engine(False).hash_key(secret, config) + assert hash.startswith(config) and len(hash) == len(config)+31 + return str_to_uascii(hash[-31:]) + + def _calc_checksum_builtin(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if _BNULL in secret: + raise uh.exc.NullPasswordError(self) + chk = _builtin_bcrypt(secret, self.ident.strip("$"), + self.salt.encode("ascii"), self.rounds) + return chk.decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/cisco.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,217 @@ +"""passlib.handlers.cisco - Cisco password hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import md5 +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import h64, right_pad_string, to_unicode +from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, join_byte_values, \ + join_byte_elems, byte_elem_value, iter_byte_values, uascii_to_str, str_to_uascii +import passlib.utils.handlers as uh +# local +__all__ = [ + "cisco_pix", + "cisco_type7", +] + +#============================================================================= +# cisco pix firewall hash +#============================================================================= +class cisco_pix(uh.HasUserContext, uh.StaticHandler): + """This class implements the password hash used by Cisco PIX firewalls, + and follows the :ref:`password-hash-api`. + It does a single round of hashing, and relies on the username + as the salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods + have the following extra keyword: + + :type user: str + :param user: + String containing name of user account this password is associated with. + + This is *required* in order to correctly hash passwords associated + with a user account on the Cisco device, as it is used to salt + the hash. + + Conversely, this *must* be omitted or set to ``""`` in order to correctly + hash passwords which don't have an associated user account + (such as the "enable" password). + """ + #=================================================================== + # class attrs + #=================================================================== + name = "cisco_pix" + checksum_size = 16 + checksum_chars = uh.HASH64_CHARS + + #=================================================================== + # methods + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + # XXX: no idea what unicode policy is, but all examples are + # 7-bit ascii compatible, so using UTF-8 + secret = secret.encode("utf-8") + + user = self.user + if user: + # NOTE: not *positive* about this, but it looks like per-user + # accounts use first 4 chars of user as salt, whereas global + # "enable" passwords don't have any salt at all. + if isinstance(user, unicode): + user = user.encode("utf-8") + secret += user[:4] + + # pad/truncate to 16 + secret = right_pad_string(secret, 16) + + # md5 digest + hash = md5(secret).digest() + + # drop every 4th byte + hash = join_byte_elems(c for i,c in enumerate(hash) if i & 3 < 3) + + # encode using Hash64 + return h64.encode_bytes(hash).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# type 7 +#============================================================================= +class cisco_type7(uh.GenericHandler): + """This class implements the Type 7 password encoding used by Cisco IOS, + and follows the :ref:`password-hash-api`. + It has a simple 4-5 bit salt, but is nonetheless a reversible encoding + instead of a real hash. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genhash` methods + have the following optional keywords: + + :type salt: int + :param salt: + This may be an optional salt integer drawn from ``range(0,16)``. + If omitted, one will be chosen at random. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` values that are out of range. + + Note that while this class outputs digests in upper-case hexidecimal, + it will accept lower-case as well. + + This class also provides the following additional method: + + .. automethod:: decode + """ + #=================================================================== + # class attrs + #=================================================================== + name = "cisco_type7" + setting_kwds = ("salt",) + checksum_chars = uh.UPPER_HEX_CHARS + + min_salt_value = 0 + max_salt_value = 52 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, config): + # special case to handle ``config=None`` in same style as StaticHandler + if config is None: + return cls.encrypt(secret) + else: + return super(cisco_type7, cls).genhash(secret, config) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + if len(hash) < 2: + raise uh.exc.InvalidHashError(cls) + salt = int(hash[:2]) # may throw ValueError + return cls(salt=salt, checksum=hash[2:].upper()) + + def __init__(self, salt=None, **kwds): + super(cisco_type7, self).__init__(**kwds) + self.salt = self._norm_salt(salt) + + def _norm_salt(self, salt): + # NOTE: the "salt" for this algorithm is a small integer. + # XXX: not entirely sure that values >15 are valid, so for + # compatibility we don't output those values but we do accept them. + if salt is None: + if self.use_defaults: + salt = self._generate_salt() + else: + raise TypeError("no salt specified") + if not isinstance(salt, int): + raise uh.exc.ExpectedTypeError(salt, "integer", "salt") + if salt < 0 or salt > self.max_salt_value: + msg = "salt/offset must be in 0..52 range" + if self.relaxed: + warn(msg, uh.PasslibHashWarning) + salt = 0 if salt < 0 else self.max_salt_value + else: + raise ValueError(msg) + return salt + + def _generate_salt(self): + return uh.rng.randint(0, 15) + + def to_string(self): + return "%02d%s" % (self.salt, uascii_to_str(self.checksum)) + + def _calc_checksum(self, secret): + # XXX: no idea what unicode policy is, but all examples are + # 7-bit ascii compatible, so using UTF-8 + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper() + + @classmethod + def decode(cls, hash, encoding="utf-8"): + """decode hash, returning original password. + + :arg hash: encoded password + :param encoding: optional encoding to use (defaults to ``UTF-8``). + :returns: password as unicode + """ + self = cls.from_string(hash) + tmp = unhexlify(self.checksum.encode("ascii")) + raw = self._cipher(tmp, self.salt) + return raw.decode(encoding) if encoding else raw + + # type7 uses a xor-based vingere variant, using the following secret key: + _key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87") + + @classmethod + def _cipher(cls, data, salt): + "xor static key against data - encrypts & decrypts" + key = cls._key + key_size = len(key) + return join_byte_values( + value ^ ord(key[(salt + idx) % key_size]) + for idx, value in enumerate(iter_byte_values(data)) + ) + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/des_crypt.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,517 @@ +"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants""" +#============================================================================= +# imports +#============================================================================= +# core +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt, to_unicode +from passlib.utils.compat import b, bytes, byte_elem_value, u, uascii_to_str, unicode +from passlib.utils.des import des_encrypt_int_block +import passlib.utils.handlers as uh +# local +__all__ = [ + "des_crypt", + "bsdi_crypt", + "bigcrypt", + "crypt16", +] + +#============================================================================= +# pure-python backend for des_crypt family +#============================================================================= +_BNULL = b('\x00') + +def _crypt_secret_to_key(secret): + """convert secret to 64-bit DES key. + + this only uses the first 8 bytes of the secret, + and discards the high 8th bit of each byte at that. + a null parity bit is inserted after every 7th bit of the output. + """ + # NOTE: this would set the parity bits correctly, + # but des_encrypt_int_block() would just ignore them... + ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8) + ## for i, c in enumerate(secret[:8])) + return sum((byte_elem_value(c) & 0x7f) << (57-i*8) + for i, c in enumerate(secret[:8])) + +def _raw_des_crypt(secret, salt): + "pure-python backed for des_crypt" + assert len(salt) == 2 + + # NOTE: some OSes will accept non-HASH64 characters in the salt, + # but what value they assign these characters varies wildy, + # so just rejecting them outright. + # NOTE: the same goes for single-character salts... + # some OSes duplicate the char, some insert a '.' char, + # and openbsd does something which creates an invalid hash. + try: + salt_value = h64.decode_int12(salt) + except ValueError: # pragma: no cover - always caught by class + raise ValueError("invalid chars in salt") + + # gotta do something - no official policy since this predates unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + assert isinstance(secret, bytes) + + # forbidding NULL char because underlying crypt() rejects them too. + if _BNULL in secret: + raise uh.exc.NullPasswordError(des_crypt) + + # convert first 8 bytes of secret string into an integer + key_value = _crypt_secret_to_key(secret) + + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, 25) + + # run h64 encode on result + return h64big.encode_int64(result) + +def _bsdi_secret_to_key(secret): + "covert secret to DES key used by bsdi_crypt" + key_value = _crypt_secret_to_key(secret) + idx = 8 + end = len(secret) + while idx < end: + next = idx+8 + tmp_value = _crypt_secret_to_key(secret[idx:next]) + key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value + idx = next + return key_value + +def _raw_bsdi_crypt(secret, rounds, salt): + "pure-python backend for bsdi_crypt" + + # decode salt + try: + salt_value = h64.decode_int24(salt) + except ValueError: # pragma: no cover - always caught by class + raise ValueError("invalid salt") + + # gotta do something - no official policy since this predates unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + assert isinstance(secret, bytes) + + # forbidding NULL char because underlying crypt() rejects them too. + if _BNULL in secret: + raise uh.exc.NullPasswordError(bsdi_crypt) + + # convert secret string into an integer + key_value = _bsdi_secret_to_key(secret) + + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, rounds) + + # run h64 encode on result + return h64big.encode_int64(result) + +#============================================================================= +# handlers +#============================================================================= +class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): + """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "des_crypt" + setting_kwds = ("salt",) + checksum_chars = uh.HASH64_CHARS + checksum_size = 11 + + #--HasSalt-- + min_salt_size = max_salt_size = 2 + salt_chars = uh.HASH64_CHARS + + #=================================================================== + # formatting + #=================================================================== + # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum + + _hash_regex = re.compile(u(r""" + ^ + (?P<salt>[./a-z0-9]{2}) + (?P<chk>[./a-z0-9]{11})? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + salt, chk = hash[:2], hash[2:] + return cls(salt=salt, checksum=chk or None) + + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backend + #=================================================================== + backends = ("os_crypt", "builtin") + + _has_backend_builtin = True + + @classproperty + def _has_backend_os_crypt(cls): + return test_crypt("test", 'abgOeLfPimXQo') + + def _calc_checksum_builtin(self, secret): + return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii") + + def _calc_checksum_os_crypt(self, secret): + # NOTE: safe_crypt encodes unicode secret -> utf8 + # no official policy since des-crypt predates unicode + hash = safe_crypt(secret, self.salt) + if hash: + assert hash.startswith(self.salt) and len(hash) == 13 + return hash[2:] + else: + return self._calc_checksum_builtin(secret) + + #=================================================================== + # eoc + #=================================================================== + +class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 5001, must be between 1 and 16777215, inclusive. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + :meth:`encrypt` will now issue a warning if an even number of rounds is used + (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys). + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "bsdi_crypt" + setting_kwds = ("salt", "rounds") + checksum_size = 11 + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + min_salt_size = max_salt_size = 4 + salt_chars = uh.HASH64_CHARS + + #--HasRounds-- + default_rounds = 5001 + min_rounds = 1 + max_rounds = 16777215 # (1<<24)-1 + rounds_cost = "linear" + + # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds, + # but that seems to be an OS policy, not a algorithm limitation. + + #=================================================================== + # parsing + #=================================================================== + _hash_regex = re.compile(u(r""" + ^ + _ + (?P<rounds>[./a-z0-9]{4}) + (?P<salt>[./a-z0-9]{4}) + (?P<chk>[./a-z0-9]{11})? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + rounds, salt, chk = m.group("rounds", "salt", "chk") + return cls( + rounds=h64.decode_int24(rounds.encode("ascii")), + salt=salt, + checksum=chk, + ) + + def to_string(self): + hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"), + self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # validation + #=================================================================== + + # flag so CryptContext won't generate even rounds. + _avoid_even_rounds = True + + def _norm_rounds(self, rounds): + rounds = super(bsdi_crypt, self)._norm_rounds(rounds) + # issue warning if app provided an even rounds value + if self.use_defaults and not rounds & 1: + warn("bsdi_crypt rounds should be odd, " + "as even rounds may reveal weak DES keys", + uh.exc.PasslibSecurityWarning) + return rounds + + @classmethod + def _bind_needs_update(cls, **settings): + return cls._needs_update + + @classmethod + def _needs_update(cls, hash, secret): + # mark bsdi_crypt hashes as deprecated if they have even rounds. + assert cls.identify(hash) + if isinstance(hash, unicode): + hash = hash.encode("ascii") + rounds = h64.decode_int24(hash[1:5]) + return not rounds & 1 + + #=================================================================== + # backends + #=================================================================== + backends = ("os_crypt", "builtin") + + _has_backend_builtin = True + + @classproperty + def _has_backend_os_crypt(cls): + return test_crypt("test", '_/...lLDAxARksGCHin.') + + def _calc_checksum_builtin(self, secret): + return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") + + def _calc_checksum_os_crypt(self, secret): + config = self.to_string() + hash = safe_crypt(secret, config) + if hash: + assert hash.startswith(config[:9]) and len(hash) == 20 + return hash[-11:] + else: + return self._calc_checksum_builtin(secret) + + #=================================================================== + # eoc + #=================================================================== + +class bigcrypt(uh.HasSalt, uh.GenericHandler): + """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "bigcrypt" + setting_kwds = ("salt",) + checksum_chars = uh.HASH64_CHARS + # NOTE: checksum chars must be multiple of 11 + + #--HasSalt-- + min_salt_size = max_salt_size = 2 + salt_chars = uh.HASH64_CHARS + + #=================================================================== + # internal helpers + #=================================================================== + _hash_regex = re.compile(u(r""" + ^ + (?P<salt>[./a-z0-9]{2}) + (?P<chk>([./a-z0-9]{11})+)? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + salt, chk = m.group("salt", "chk") + return cls(salt=salt, checksum=chk) + + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + def _norm_checksum(self, value): + value = super(bigcrypt, self)._norm_checksum(value) + if value and len(value) % 11: + raise uh.exc.InvalidHashError(self) + return value + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + chk = _raw_des_crypt(secret, self.salt.encode("ascii")) + idx = 8 + end = len(secret) + while idx < end: + next = idx + 8 + chk += _raw_des_crypt(secret[idx:next], chk[-11:-9]) + idx = next + return chk.decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +class crypt16(uh.HasSalt, uh.GenericHandler): + """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "crypt16" + setting_kwds = ("salt",) + checksum_size = 22 + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + min_salt_size = max_salt_size = 2 + salt_chars = uh.HASH64_CHARS + + #=================================================================== + # internal helpers + #=================================================================== + _hash_regex = re.compile(u(r""" + ^ + (?P<salt>[./a-z0-9]{2}) + (?P<chk>[./a-z0-9]{22})? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + salt, chk = m.group("salt", "chk") + return cls(salt=salt, checksum=chk) + + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + + # parse salt value + try: + salt_value = h64.decode_int12(self.salt.encode("ascii")) + except ValueError: # pragma: no cover - caught by class + raise ValueError("invalid chars in salt") + + # convert first 8 byts of secret string into an integer, + key1 = _crypt_secret_to_key(secret) + + # run data through des using input of 0 + result1 = des_encrypt_int_block(key1, 0, salt_value, 20) + + # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars) + key2 = _crypt_secret_to_key(secret[8:16]) + + # run data through des using input of 0 + result2 = des_encrypt_int_block(key2, 0, salt_value, 5) + + # done + chk = h64big.encode_int64(result1) + h64big.encode_int64(result2) + return chk.decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/digests.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,144 @@ +"""passlib.handlers.digests - plain hash digests +""" +#============================================================================= +# imports +#============================================================================= +# core +import hashlib +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_native_str, to_bytes, render_bytes, consteq +from passlib.utils.compat import bascii_to_str, bytes, unicode, str_to_uascii +import passlib.utils.handlers as uh +from passlib.utils.md4 import md4 +# local +__all__ = [ + "create_hex_hash", + "hex_md4", + "hex_md5", + "hex_sha1", + "hex_sha256", + "hex_sha512", +] + +#============================================================================= +# helpers for hexidecimal hashes +#============================================================================= +class HexDigestHash(uh.StaticHandler): + "this provides a template for supporting passwords stored as plain hexidecimal hashes" + #=================================================================== + # class attrs + #=================================================================== + _hash_func = None # hash function to use - filled in by create_hex_hash() + checksum_size = None # filled in by create_hex_hash() + checksum_chars = uh.HEX_CHARS + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(self._hash_func(secret).hexdigest()) + + #=================================================================== + # eoc + #=================================================================== + +def create_hex_hash(hash, digest_name, module=__name__): + # NOTE: could set digest_name=hash.name for cpython, but not for some other platforms. + h = hash() + name = "hex_" + digest_name + return type(name, (HexDigestHash,), dict( + name=name, + __module__=module, # so ABCMeta won't clobber it + _hash_func=staticmethod(hash), # sometimes it's a function, sometimes not. so wrap it. + checksum_size=h.digest_size*2, + __doc__="""This class implements a plain hexidecimal %s hash, and follows the :ref:`password-hash-api`. + +It supports no optional or contextual keywords. +""" % (digest_name,) + )) + +#============================================================================= +# predefined handlers +#============================================================================= +hex_md4 = create_hex_hash(md4, "md4") +hex_md5 = create_hex_hash(hashlib.md5, "md5") +hex_md5.django_name = "unsalted_md5" +hex_sha1 = create_hex_hash(hashlib.sha1, "sha1") +hex_sha256 = create_hex_hash(hashlib.sha256, "sha256") +hex_sha512 = create_hex_hash(hashlib.sha512, "sha512") + +#============================================================================= +# htdigest +#============================================================================= +class htdigest(uh.PasswordHash): + """htdigest hash function. + + .. todo:: + document this hash + """ + name = "htdigest" + setting_kwds = () + context_kwds = ("user", "realm", "encoding") + default_encoding = "utf-8" + + @classmethod + def encrypt(cls, secret, user, realm, encoding=None): + # NOTE: this was deliberately written so that raw bytes are passed through + # unchanged, the encoding kwd is only used to handle unicode values. + if not encoding: + encoding = cls.default_encoding + uh.validate_secret(secret) + if isinstance(secret, unicode): + secret = secret.encode(encoding) + user = to_bytes(user, encoding, "user") + realm = to_bytes(realm, encoding, "realm") + data = render_bytes("%s:%s:%s", user, realm, secret) + return hashlib.md5(data).hexdigest() + + @classmethod + def _norm_hash(cls, hash): + "normalize hash to native string, and validate it" + hash = to_native_str(hash, param="hash") + if len(hash) != 32: + raise uh.exc.MalformedHashError(cls, "wrong size") + for char in hash: + if char not in uh.LC_HEX_CHARS: + raise uh.exc.MalformedHashError(cls, "invalid chars in hash") + return hash + + @classmethod + def verify(cls, secret, hash, user, realm, encoding="utf-8"): + hash = cls._norm_hash(hash) + other = cls.encrypt(secret, user, realm, encoding) + return consteq(hash, other) + + @classmethod + def identify(cls, hash): + try: + cls._norm_hash(hash) + except ValueError: + return False + return True + + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, config, user, realm, encoding="utf-8"): + if config is not None: + cls._norm_hash(config) + return cls.encrypt(secret, user, realm, encoding) + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/django.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,377 @@ +"""passlib.handlers.django- Django password hash support""" +#============================================================================= +# imports +#============================================================================= +# core +from base64 import b64encode +from hashlib import md5, sha1 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_unicode, classproperty +from passlib.utils.compat import b, bytes, str_to_uascii, uascii_to_str, unicode, u +from passlib.utils.pbkdf2 import pbkdf2 +import passlib.utils.handlers as uh +# local +__all__ = [ + "django_salted_sha1", + "django_salted_md5", + "django_bcrypt", + "django_pbkdf2_sha1", + "django_pbkdf2_sha256", + "django_des_crypt", + "django_disabled", +] + +#============================================================================= +# lazy imports & constants +#============================================================================= +des_crypt = None + +def _import_des_crypt(): + global des_crypt + if des_crypt is None: + from passlib.hash import des_crypt + return des_crypt + +# django 1.4's salt charset +SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + +#============================================================================= +# salted hashes +#============================================================================= +class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler): + """base class providing common code for django hashes""" + # name, ident, checksum_size must be set by subclass. + # ident must include "$" suffix. + setting_kwds = ("salt", "salt_size") + + min_salt_size = 0 + # NOTE: django 1.0-1.3 would accept empty salt strings. + # django 1.4 won't, but this appears to be regression + # (https://code.djangoproject.com/ticket/18144) + # so presumably it will be fixed in a later release. + default_salt_size = 12 + max_salt_size = None + salt_chars = SALT_CHARS + + checksum_chars = uh.LOWER_HEX_CHARS + + @classproperty + def _stub_checksum(cls): + return cls.checksum_chars[0] * cls.checksum_size + + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + return cls(salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc2(self.ident, self.salt, + self.checksum or self._stub_checksum) + +class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash): + """base class providing common code for django hashes w/ variable rounds""" + setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",) + + min_rounds = 1 + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc3(self.ident, self.rounds, self.salt, + self.checksum or self._stub_checksum) + +class django_salted_sha1(DjangoSaltedHash): + """This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and uses a single round of SHA1. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + This should be compatible with Django 1.4's :class:`!SHA1PasswordHasher` class. + + .. versionchanged: 1.6 + This class now generates 12-character salts instead of 5, + and generated salts uses the character range ``[0-9a-zA-Z]`` instead of + the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4 + generates these hashes; but hashes generated in this manner will still be + correctly interpreted by earlier versions of Django. + """ + name = "django_salted_sha1" + django_name = "sha1" + ident = u("sha1$") + checksum_size = 40 + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(sha1(self.salt.encode("ascii") + secret).hexdigest()) + +class django_salted_md5(DjangoSaltedHash): + """This class implements Django's Salted MD5 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and uses a single round of MD5. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!MD5PasswordHasher` class. + + .. versionchanged: 1.6 + This class now generates 12-character salts instead of 5, + and generated salts uses the character range ``[0-9a-zA-Z]`` instead of + the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4 + generates these hashes; but hashes generated in this manner will still be + correctly interpreted by earlier versions of Django. + """ + name = "django_salted_md5" + django_name = "md5" + ident = u("md5$") + checksum_size = 32 + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest()) + +django_bcrypt = uh.PrefixWrapper("django_bcrypt", "bcrypt", + prefix=u('bcrypt$'), ident=u("bcrypt$"), + # NOTE: this docstring is duplicated in the docs, since sphinx + # seems to be having trouble reading it via autodata:: + doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`. + + This is identical to :class:`!bcrypt` itself, but with + the Django-specific prefix ``"bcrypt$"`` prepended. + + See :doc:`/lib/passlib.hash.bcrypt` for more details, + the usage and behavior is identical. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!BCryptPasswordHasher` class. + + .. versionadded:: 1.6 + """) +django_bcrypt.django_name = "bcrypt" + +class django_pbkdf2_sha256(DjangoVariableHash): + """This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 10000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha256" + django_name = "pbkdf2_sha256" + ident = u('pbkdf2_sha256$') + min_salt_size = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + checksum_chars = uh.PADDED_BASE64_CHARS + checksum_size = 44 # 32 bytes -> base64 + default_rounds = 10000 # NOTE: using django default here + _prf = "hmac-sha256" + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + hash = pbkdf2(secret, self.salt.encode("ascii"), self.rounds, + keylen=None, prf=self._prf) + return b64encode(hash).rstrip().decode("ascii") + +class django_pbkdf2_sha1(django_pbkdf2_sha256): + """This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 10000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha1" + django_name = "pbkdf2_sha1" + ident = u('pbkdf2_sha1$') + checksum_size = 28 # 20 bytes -> base64 + _prf = "hmac-sha1" + +#============================================================================= +# other +#============================================================================= +class django_des_crypt(uh.HasSalt, uh.GenericHandler): + """This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!CryptPasswordHasher` class. + Note that Django only supports this hash on Unix systems + (though :class:`!django_des_crypt` is available cross-platform + under Passlib). + + .. versionchanged:: 1.6 + This class will now accept hashes with empty salt strings, + since Django 1.4 generates them this way. + """ + name = "django_des_crypt" + django_name = "crypt" + setting_kwds = ("salt", "salt_size") + ident = u("crypt$") + checksum_chars = salt_chars = uh.HASH64_CHARS + checksum_size = 11 + min_salt_size = default_salt_size = 2 + _stub_checksum = u('.')*11 + + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + if chk: + # chk should be full des_crypt hash + if not salt: + # django 1.4 always uses empty salt field, + # so extract salt from des_crypt hash <chk> + salt = chk[:2] + elif salt[:2] != chk[:2]: + # django 1.0 stored 5 chars in salt field, and duplicated + # the first two chars in <chk>. we keep the full salt, + # but make sure the first two chars match as sanity check. + raise uh.exc.MalformedHashError(cls, + "first two digits of salt and checksum must match") + # in all cases, strip salt chars from <chk> + chk = chk[2:] + return cls(salt=salt, checksum=chk) + + def to_string(self): + # NOTE: always filling in salt field, so that we're compatible + # with django 1.0 (which requires it) + salt = self.salt + chk = salt[:2] + (self.checksum or self._stub_checksum) + return uh.render_mc2(self.ident, salt, chk) + + def _calc_checksum(self, secret): + # NOTE: we lazily import des_crypt, + # since most django deploys won't use django_des_crypt + global des_crypt + if des_crypt is None: + _import_des_crypt() + return des_crypt(salt=self.salt[:2])._calc_checksum(secret) + +class django_disabled(uh.StaticHandler): + """This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`. + + This class does not implement a hash, but instead + claims the special hash string ``"!"`` which Django uses + to indicate an account's password has been disabled. + + * newly encrypted passwords will hash to ``!``. + * it rejects all passwords. + """ + name = "django_disabled" + + @classmethod + def identify(cls, hash): + hash = uh.to_unicode_for_identify(hash) + return hash == u("!") + + def _calc_checksum(self, secret): + return u("!") + + @classmethod + def verify(cls, secret, hash): + uh.validate_secret(secret) + if not cls.identify(hash): + raise uh.exc.InvalidHashError(cls) + return False + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/fshp.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,206 @@ +"""passlib.handlers.fshp +""" + +#============================================================================= +# imports +#============================================================================= +# core +from base64 import b64encode, b64decode +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_unicode +import passlib.utils.handlers as uh +from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, u,\ + unicode +from passlib.utils.pbkdf2 import pbkdf1 +# local +__all__ = [ + 'fshp', +] +#============================================================================= +# sha1-crypt +#============================================================================= +class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the FSHP password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :param salt: + Optional raw salt string. + If not specified, one will be autogenerated (this is recommended). + + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any non-negative value. + + :param rounds: + Optional number of rounds to use. + Defaults to 50000, must be between 1 and 4294967295, inclusive. + + :param variant: + Optionally specifies variant of FSHP to use. + + * ``0`` - uses SHA-1 digest (deprecated). + * ``1`` - uses SHA-2/256 digest (default). + * ``2`` - uses SHA-2/384 digest. + * ``3`` - uses SHA-2/512 digest. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "fshp" + setting_kwds = ("salt", "salt_size", "rounds", "variant") + checksum_chars = uh.PADDED_BASE64_CHARS + ident = u("{FSHP") + # checksum_size is property() that depends on variant + + #--HasRawSalt-- + default_salt_size = 16 # current passlib default, FSHP uses 8 + min_salt_size = 0 + max_salt_size = None + + #--HasRounds-- + # FIXME: should probably use different default rounds + # based on the variant. setting for default variant (sha256) for now. + default_rounds = 50000 # current passlib default, FSHP uses 4096 + min_rounds = 1 # set by FSHP + max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP + rounds_cost = "linear" + + #--variants-- + default_variant = 1 + _variant_info = { + # variant: (hash name, digest size) + 0: ("sha1", 20), + 1: ("sha256", 32), + 2: ("sha384", 48), + 3: ("sha512", 64), + } + _variant_aliases = dict( + [(unicode(k),k) for k in _variant_info] + + [(v[0],k) for k,v in iteritems(_variant_info)] + ) + + #=================================================================== + # instance attrs + #=================================================================== + variant = None + + #=================================================================== + # init + #=================================================================== + def __init__(self, variant=None, **kwds): + # NOTE: variant must be set first, since it controls checksum size, etc. + self.use_defaults = kwds.get("use_defaults") # load this early + self.variant = self._norm_variant(variant) + super(fshp, self).__init__(**kwds) + + def _norm_variant(self, variant): + if variant is None: + if not self.use_defaults: + raise TypeError("no variant specified") + variant = self.default_variant + if isinstance(variant, bytes): + variant = variant.decode("ascii") + if isinstance(variant, unicode): + try: + variant = self._variant_aliases[variant] + except KeyError: + raise ValueError("invalid fshp variant") + if not isinstance(variant, int): + raise TypeError("fshp variant must be int or known alias") + if variant not in self._variant_info: + raise ValueError("invalid fshp variant") + return variant + + @property + def checksum_alg(self): + return self._variant_info[self.variant][0] + + @property + def checksum_size(self): + return self._variant_info[self.variant][1] + + #=================================================================== + # formatting + #=================================================================== + + _hash_regex = re.compile(u(r""" + ^ + \{FSHP + (\d+)\| # variant + (\d+)\| # salt size + (\d+)\} # rounds + ([a-zA-Z0-9+/]+={0,3}) # digest + $"""), re.X) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + variant, salt_size, rounds, data = m.group(1,2,3,4) + variant = int(variant) + salt_size = int(salt_size) + rounds = int(rounds) + try: + data = b64decode(data.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + salt = data[:salt_size] + chk = data[salt_size:] + return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant) + + @property + def _stub_checksum(self): + return b('\x00') * self.checksum_size + + def to_string(self): + chk = self.checksum or self._stub_checksum + salt = self.salt + data = bascii_to_str(b64encode(salt+chk)) + return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data) + + #=================================================================== + # backend + #=================================================================== + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + # NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed. + # this has only a minimal impact on security, + # but it is worth noting this deviation. + return pbkdf1( + secret=self.salt, + salt=secret, + rounds=self.rounds, + keylen=self.checksum_size, + hash=self.checksum_alg, + ) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/ldap_digests.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,270 @@ +"""passlib.handlers.digests - plain hash digests +""" +#============================================================================= +# imports +#============================================================================= +# core +from base64 import b64encode, b64decode +from hashlib import md5, sha1 +import logging; log = logging.getLogger(__name__) +import re +from warnings import warn +# site +# pkg +from passlib.handlers.misc import plaintext +from passlib.utils import to_native_str, unix_crypt_schemes, \ + classproperty, to_unicode +from passlib.utils.compat import b, bytes, uascii_to_str, unicode, u +import passlib.utils.handlers as uh +# local +__all__ = [ + "ldap_plaintext", + "ldap_md5", + "ldap_sha1", + "ldap_salted_md5", + "ldap_salted_sha1", + + ##"get_active_ldap_crypt_schemes", + "ldap_des_crypt", + "ldap_bsdi_crypt", + "ldap_md5_crypt", + "ldap_sha1_crypt" + "ldap_bcrypt", + "ldap_sha256_crypt", + "ldap_sha512_crypt", +] + +#============================================================================= +# ldap helpers +#============================================================================= +class _Base64DigestHelper(uh.StaticHandler): + "helper for ldap_md5 / ldap_sha1" + # XXX: could combine this with hex digests in digests.py + + ident = None # required - prefix identifier + _hash_func = None # required - hash function + _hash_regex = None # required - regexp to recognize hash + checksum_chars = uh.PADDED_BASE64_CHARS + + @classproperty + def _hash_prefix(cls): + "tell StaticHandler to strip ident from checksum" + return cls.ident + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + chk = self._hash_func(secret).digest() + return b64encode(chk).decode("ascii") + +class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + "helper for ldap_salted_md5 / ldap_salted_sha1" + setting_kwds = ("salt", "salt_size") + checksum_chars = uh.PADDED_BASE64_CHARS + + ident = None # required - prefix identifier + checksum_size = None # required + _hash_func = None # required - hash function + _hash_regex = None # required - regexp to recognize hash + _stub_checksum = None # required - default checksum to plug in + min_salt_size = max_salt_size = 4 + + # NOTE: openldap implementation uses 4 byte salt, + # but it's been reported (issue 30) that some servers use larger salts. + # the semi-related rfc3112 recommends support for up to 16 byte salts. + min_salt_size = 4 + default_salt_size = 4 + max_salt_size = 16 + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + try: + data = b64decode(m.group("tmp").encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + cs = cls.checksum_size + assert cs + return cls(checksum=data[:cs], salt=data[cs:]) + + def to_string(self): + data = (self.checksum or self._stub_checksum) + self.salt + hash = self.ident + b64encode(data).decode("ascii") + return uascii_to_str(hash) + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return self._hash_func(secret + self.salt).digest() + +#============================================================================= +# implementations +#============================================================================= +class ldap_md5(_Base64DigestHelper): + """This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. + """ + name = "ldap_md5" + ident = u("{MD5}") + _hash_func = md5 + _hash_regex = re.compile(u(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$")) + +class ldap_sha1(_Base64DigestHelper): + """This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. + """ + name = "ldap_sha1" + ident = u("{SHA}") + _hash_func = sha1 + _hash_regex = re.compile(u(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$")) + +class ldap_salted_md5(_SaltedBase64DigestHelper): + """This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`. + + It supports a 4-16 byte salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 4 bytes for compatibility with the LDAP spec, + but some systems use larger salts, and Passlib supports + any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This format now supports variable length salts, instead of a fix 4 bytes. + """ + name = "ldap_salted_md5" + ident = u("{SMD5}") + checksum_size = 16 + _hash_func = md5 + _hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27,}={0,2})$")) + _stub_checksum = b('\x00') * 16 + +class ldap_salted_sha1(_SaltedBase64DigestHelper): + """This class stores passwords using LDAP's salted SHA1 format, and follows the :ref:`password-hash-api`. + + It supports a 4-16 byte salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 4 bytes for compatibility with the LDAP spec, + but some systems use larger salts, and Passlib supports + any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This format now supports variable length salts, instead of a fix 4 bytes. + """ + name = "ldap_salted_sha1" + ident = u("{SSHA}") + checksum_size = 20 + _hash_func = sha1 + _hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32,}={0,2})$")) + _stub_checksum = b('\x00') * 20 + +class ldap_plaintext(plaintext): + """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. + + This class acts much like the generic :class:`!passlib.hash.plaintext` handler, + except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix + used by RFC2307 passwords. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keyword: + + :type encoding: str + :param encoding: + This controls the character encoding to use (defaults to ``utf-8``). + + This encoding will be used to encode :class:`!unicode` passwords + under Python 2, and decode :class:`!bytes` hashes under Python 3. + + .. versionchanged:: 1.6 + The ``encoding`` keyword was added. + """ + # NOTE: this subclasses plaintext, since all it does differently + # is override identify() + + name = "ldap_plaintext" + _2307_pat = re.compile(u(r"^\{\w+\}.*$")) + + @classmethod + def identify(cls, hash): + # NOTE: identifies all strings EXCEPT those with {XXX} prefix + hash = uh.to_unicode_for_identify(hash) + return bool(hash) and cls._2307_pat.match(hash) is None + +#============================================================================= +# {CRYPT} wrappers +# the following are wrappers around the base crypt algorithms, +# which add the ldap required {CRYPT} prefix +#============================================================================= +ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ] + +def _init_ldap_crypt_handlers(): + # NOTE: I don't like to implicitly modify globals() like this, + # but don't want to write out all these handlers out either :) + g = globals() + for wname in unix_crypt_schemes: + name = 'ldap_' + wname + g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True) + del g +_init_ldap_crypt_handlers() + +##_lcn_host = None +##def get_host_ldap_crypt_schemes(): +## global _lcn_host +## if _lcn_host is None: +## from passlib.hosts import host_context +## schemes = host_context.schemes() +## _lcn_host = [ +## "ldap_" + name +## for name in unix_crypt_names +## if name in schemes +## ] +## return _lcn_host + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/md5_crypt.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,333 @@ +"""passlib.handlers.md5_crypt - md5-crypt algorithm""" +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import md5 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import classproperty, h64, safe_crypt, test_crypt, repeat_string +from passlib.utils.compat import b, bytes, irange, unicode, u +import passlib.utils.handlers as uh +# local +__all__ = [ + "md5_crypt", + "apr_md5_crypt", +] + +#============================================================================= +# pure-python backend +#============================================================================= +_BNULL = b("\x00") +_MD5_MAGIC = b("$1$") +_APR_MAGIC = b("$apr1$") + +# pre-calculated offsets used to speed up C digest stage (see notes below). +# sequence generated using the following: + ##perms_order = "p,pp,ps,psp,sp,spp".split(",") + ##def offset(i): + ## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") + + ## ("p" if i % 7 else "") + ("" if i % 2 else "p")) + ## return perms_order.index(key) + ##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)] +_c_digest_offsets = ( + (0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3), + (4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1), + (4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3), + ) + +# map used to transpose bytes when encoding final digest +_transpose_map = (12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11) + +def _raw_md5_crypt(pwd, salt, use_apr=False): + """perform raw md5-crypt calculation + + this function provides a pure-python implementation of the internals + for the MD5-Crypt algorithms; it doesn't handle any of the + parsing/validation of the hash strings themselves. + + :arg pwd: password chars/bytes to encrypt + :arg salt: salt chars to use + :arg use_apr: use apache variant + + :returns: + encoded checksum chars + """ + # NOTE: regarding 'apr' format: + # really, apache? you had to invent a whole new "$apr1$" format, + # when all you did was change the ident incorporated into the hash? + # would love to find webpage explaining why just using a portable + # implementation of $1$ wasn't sufficient. *nothing* else was changed. + + #=================================================================== + # init & validate inputs + #=================================================================== + + # validate secret + # XXX: not sure what official unicode policy is, using this as default + if isinstance(pwd, unicode): + pwd = pwd.encode("utf-8") + assert isinstance(pwd, bytes), "pwd not unicode or bytes" + if _BNULL in pwd: + raise uh.exc.NullPasswordError(md5_crypt) + pwd_len = len(pwd) + + # validate salt - should have been taken care of by caller + assert isinstance(salt, unicode), "salt not unicode" + salt = salt.encode("ascii") + assert len(salt) < 9, "salt too large" + # NOTE: spec says salts larger than 8 bytes should be truncated, + # instead of causing an error. this function assumes that's been + # taken care of by the handler class. + + # load APR specific constants + if use_apr: + magic = _APR_MAGIC + else: + magic = _MD5_MAGIC + + #=================================================================== + # digest B - used as subinput to digest A + #=================================================================== + db = md5(pwd + salt + pwd).digest() + + #=================================================================== + # digest A - used to initialize first round of digest C + #=================================================================== + # start out with pwd + magic + salt + a_ctx = md5(pwd + magic + salt) + a_ctx_update = a_ctx.update + + # add pwd_len bytes of b, repeating b as many times as needed. + a_ctx_update(repeat_string(db, pwd_len)) + + # add null chars & first char of password + # NOTE: this may have historically been a bug, + # where they meant to use db[0] instead of B_NULL, + # but the original code memclear'ed db, + # and now all implementations have to use this. + i = pwd_len + evenchar = pwd[:1] + while i: + a_ctx_update(_BNULL if i & 1 else evenchar) + i >>= 1 + + # finish A + da = a_ctx.digest() + + #=================================================================== + # digest C - for a 1000 rounds, combine A, S, and P + # digests in various ways; in order to burn CPU time. + #=================================================================== + + # NOTE: the original MD5-Crypt implementation performs the C digest + # calculation using the following loop: + # + ##dc = da + ##i = 0 + ##while i < rounds: + ## tmp_ctx = md5(pwd if i & 1 else dc) + ## if i % 3: + ## tmp_ctx.update(salt) + ## if i % 7: + ## tmp_ctx.update(pwd) + ## tmp_ctx.update(dc if i & 1 else pwd) + ## dc = tmp_ctx.digest() + ## i += 1 + # + # The code Passlib uses (below) implements an equivalent algorithm, + # it's just been heavily optimized to pre-calculate a large number + # of things beforehand. It works off of a couple of observations + # about the original algorithm: + # + # 1. each round is a combination of 'dc', 'salt', and 'pwd'; determined + # by the whether 'i' a multiple of 2,3, and/or 7. + # 2. since lcm(2,3,7)==42, the series of combinations will repeat + # every 42 rounds. + # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)'; + # while odd rounds 1-41 consist of hash(round-specific-constant + dc) + # + # Using these observations, the following code... + # * calculates the round-specific combination of salt & pwd for each round 0-41 + # * runs through as many 42-round blocks as possible + # * runs through as many pairs of rounds as possible for remaining rounds + # * performs once last round if the total rounds should be odd. + # + # this cuts out a lot of the control overhead incurred when running the + # original loop 40,000+ times in python, resulting in ~20% increase in + # speed under CPython (though still 2x slower than glibc crypt) + + # prepare the 6 combinations of pwd & salt which are needed + # (order of 'perms' must match how _c_digest_offsets was generated) + pwd_pwd = pwd+pwd + pwd_salt = pwd+salt + perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd] + + # build up list of even-round & odd-round constants, + # and store in 21-element list as (even,odd) pairs. + data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets] + + # perform 23 blocks of 42 rounds each (for a total of 966 rounds) + dc = da + blocks = 23 + while blocks: + for even, odd in data: + dc = md5(odd + md5(dc + even).digest()).digest() + blocks -= 1 + + # perform 17 more pairs of rounds (34 more rounds, for a total of 1000) + for even, odd in data[:17]: + dc = md5(odd + md5(dc + even).digest()).digest() + + #=================================================================== + # encode digest using appropriate transpose map + #=================================================================== + return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii") + +#============================================================================= +# handler +#============================================================================= +class _MD5_Common(uh.HasSalt, uh.GenericHandler): + "common code for md5_crypt and apr_md5_crypt" + #=================================================================== + # class attrs + #=================================================================== + # name - set in subclass + setting_kwds = ("salt", "salt_size") + # ident - set in subclass + checksum_size = 22 + checksum_chars = uh.HASH64_CHARS + + min_salt_size = 0 + max_salt_size = 8 + salt_chars = uh.HASH64_CHARS + + #=================================================================== + # methods + #=================================================================== + + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + return cls(salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc2(self.ident, self.salt, self.checksum) + + # _calc_checksum() - provided by subclass + + #=================================================================== + # eoc + #=================================================================== + +class md5_crypt(uh.HasManyBackends, _MD5_Common): + """This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 8, but can be any value between 0 and 8. + (This is mainly needed when generating Cisco-compatible hashes, + which require ``salt_size=4``). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + name = "md5_crypt" + ident = u("$1$") + + #=================================================================== + # methods + #=================================================================== + # FIXME: can't find definitive policy on how md5-crypt handles non-ascii. + # all backends currently coerce -> utf-8 + + backends = ("os_crypt", "builtin") + + _has_backend_builtin = True + + @classproperty + def _has_backend_os_crypt(cls): + return test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/') + + def _calc_checksum_builtin(self, secret): + return _raw_md5_crypt(secret, self.salt) + + def _calc_checksum_os_crypt(self, secret): + config = self.ident + self.salt + hash = safe_crypt(secret, config) + if hash: + assert hash.startswith(config) and len(hash) == len(config) + 23 + return hash[-22:] + else: + return self._calc_checksum_builtin(secret) + + #=================================================================== + # eoc + #=================================================================== + +class apr_md5_crypt(_MD5_Common): + """This class implements the Apr-MD5-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + name = "apr_md5_crypt" + ident = u("$apr1$") + + #=================================================================== + # methods + #=================================================================== + def _calc_checksum(self, secret): + return _raw_md5_crypt(secret, self.salt, use_apr=True) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/misc.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,242 @@ +"""passlib.handlers.misc - misc generic handlers +""" +#============================================================================= +# imports +#============================================================================= +# core +import sys +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_native_str, consteq +from passlib.utils.compat import bytes, unicode, u, b, base_string_types +import passlib.utils.handlers as uh +# local +__all__ = [ + "unix_disabled", + "unix_fallback", + "plaintext", +] + +#============================================================================= +# handler +#============================================================================= +class unix_fallback(uh.StaticHandler): + """This class provides the fallback behavior for unix shadow files, and follows the :ref:`password-hash-api`. + + This class does not implement a hash, but instead provides fallback + behavior as found in /etc/shadow on most unix variants. + If used, should be the last scheme in the context. + + * this class will positive identify all hash strings. + * for security, newly encrypted passwords will hash to ``!``. + * it rejects all passwords if the hash is NOT an empty string (``!`` or ``*`` are frequently used). + * by default it rejects all passwords if the hash is an empty string, + but if ``enable_wildcard=True`` is passed to verify(), + all passwords will be allowed through if the hash is an empty string. + + .. deprecated:: 1.6 + This has been deprecated due to it's "wildcard" feature, + and will be removed in Passlib 1.8. Use :class:`unix_disabled` instead. + """ + name = "unix_fallback" + context_kwds = ("enable_wildcard",) + + @classmethod + def identify(cls, hash): + if isinstance(hash, base_string_types): + return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + + def __init__(self, enable_wildcard=False, **kwds): + warn("'unix_fallback' is deprecated, " + "and will be removed in Passlib 1.8; " + "please use 'unix_disabled' instead.", + DeprecationWarning) + super(unix_fallback, self).__init__(**kwds) + self.enable_wildcard = enable_wildcard + + @classmethod + def genhash(cls, secret, config): + # override default to preserve checksum + if config is None: + return cls.encrypt(secret) + else: + uh.validate_secret(secret) + self = cls.from_string(config) + self.checksum = self._calc_checksum(secret) + return self.to_string() + + def _calc_checksum(self, secret): + if self.checksum: + # NOTE: hash will generally be "!", but we want to preserve + # it in case it's something else, like "*". + return self.checksum + else: + return u("!") + + @classmethod + def verify(cls, secret, hash, enable_wildcard=False): + uh.validate_secret(secret) + if not isinstance(hash, base_string_types): + raise uh.exc.ExpectedStringError(hash, "hash") + elif hash: + return False + else: + return enable_wildcard + +_MARKER_CHARS = u("*!") +_MARKER_BYTES = b("*!") + +class unix_disabled(uh.PasswordHash): + """This class provides disabled password behavior for unix shadow files, + and follows the :ref:`password-hash-api`. + + This class does not implement a hash, but instead matches the "disabled account" + strings found in ``/etc/shadow`` on most Unix variants. "encrypting" a password + will simply return the disabled account marker. It will reject all passwords, + no matter the hash string. The :meth:`~passlib.ifc.PasswordHash.encrypt` + method supports one optional keyword: + + :type marker: str + :param marker: + Optional marker string which overrides the platform default + used to indicate a disabled account. + + If not specified, this will default to ``"*"`` on BSD systems, + and use the Linux default ``"!"`` for all other platforms. + (:attr:`!unix_disabled.default_marker` will contain the default value) + + .. versionadded:: 1.6 + This class was added as a replacement for the now-deprecated + :class:`unix_fallback` class, which had some undesirable features. + """ + name = "unix_disabled" + setting_kwds = ("marker",) + context_kwds = () + + if 'bsd' in sys.platform: # pragma: no cover -- runtime detection + default_marker = u("*") + else: + # use the linux default for other systems + # (glibc also supports adding old hash after the marker + # so it can be restored later). + default_marker = u("!") + + @classmethod + def identify(cls, hash): + # NOTE: technically, anything in the /etc/shadow password field + # which isn't valid crypt() output counts as "disabled". + # but that's rather ambiguous, and it's hard to predict what + # valid output is for unknown crypt() implementations. + # so to be on the safe side, we only match things *known* + # to be disabled field indicators, and will add others + # as they are found. things beginning w/ "$" should *never* match. + # + # things currently matched: + # * linux uses "!" + # * bsd uses "*" + # * linux may use "!" + hash to disable but preserve original hash + # * linux counts empty string as "any password" + if isinstance(hash, unicode): + start = _MARKER_CHARS + elif isinstance(hash, bytes): + start = _MARKER_BYTES + else: + raise uh.exc.ExpectedStringError(hash, "hash") + return not hash or hash[0] in start + + @classmethod + def encrypt(cls, secret, marker=None): + return cls.genhash(secret, None, marker) + + @classmethod + def verify(cls, secret, hash): + uh.validate_secret(secret) + if not cls.identify(hash): # handles typecheck + raise uh.exc.InvalidHashError(cls) + return False + + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, config, marker=None): + uh.validate_secret(secret) + if config is not None and not cls.identify(config): # handles typecheck + raise uh.exc.InvalidHashError(cls) + if config: + # we want to preserve the existing str, + # since it might contain a disabled password hash ("!" + hash) + return to_native_str(config, param="config") + # if None or empty string, replace with marker + if marker: + if not cls.identify(marker): + raise ValueError("invalid marker: %r" % marker) + else: + marker = cls.default_marker + assert marker and cls.identify(marker) + return to_native_str(marker, param="marker") + +class plaintext(uh.PasswordHash): + """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keyword: + + :type encoding: str + :param encoding: + This controls the character encoding to use (defaults to ``utf-8``). + + This encoding will be used to encode :class:`!unicode` passwords + under Python 2, and decode :class:`!bytes` hashes under Python 3. + + .. versionchanged:: 1.6 + The ``encoding`` keyword was added. + """ + # NOTE: this is subclassed by ldap_plaintext + + name = "plaintext" + setting_kwds = () + context_kwds = ("encoding",) + default_encoding = "utf-8" + + @classmethod + def identify(cls, hash): + if isinstance(hash, base_string_types): + return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + + @classmethod + def encrypt(cls, secret, encoding=None): + uh.validate_secret(secret) + if not encoding: + encoding = cls.default_encoding + return to_native_str(secret, encoding, "secret") + + @classmethod + def verify(cls, secret, hash, encoding=None): + if not encoding: + encoding = cls.default_encoding + hash = to_native_str(hash, encoding, "hash") + if not cls.identify(hash): + raise uh.exc.InvalidHashError(cls) + return consteq(cls.encrypt(secret, encoding), hash) + + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, hash, encoding=None): + if hash is not None and not cls.identify(hash): + raise uh.exc.InvalidHashError(cls) + return cls.encrypt(secret, encoding) + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/mssql.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,246 @@ +"""passlib.handlers.mssql - MS-SQL Password Hash + +Notes +===== +MS-SQL has used a number of hash algs over the years, +most of which were exposed through the undocumented +'pwdencrypt' and 'pwdcompare' sql functions. + +Known formats +------------- +6.5 + snefru hash, ascii encoded password + no examples found + +7.0 + snefru hash, unicode (what encoding?) + saw ref that these blobs were 16 bytes in size + no examples found + +2000 + byte string using displayed as 0x hex, using 0x0100 prefix. + contains hashes of password and upper-case password. + +2007 + same as 2000, but without the upper-case hash. + +refs +---------- +https://blogs.msdn.com/b/lcris/archive/2007/04/30/sql-server-2005-about-login-password-hashes.aspx?Redirected=true +http://us.generation-nt.com/securing-passwords-hash-help-35429432.html +http://forum.md5decrypter.co.uk/topic230-mysql-and-mssql-get-password-hashes.aspx +http://www.theregister.co.uk/2002/07/08/cracking_ms_sql_server_passwords/ +""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import sha1 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import consteq +from passlib.utils.compat import b, bytes, bascii_to_str, unicode, u +import passlib.utils.handlers as uh +# local +__all__ = [ + "mssql2000", + "mssql2005", +] + +#============================================================================= +# mssql 2000 +#============================================================================= +def _raw_mssql(secret, salt): + assert isinstance(secret, unicode) + assert isinstance(salt, bytes) + return sha1(secret.encode("utf-16-le") + salt).digest() + +BIDENT = b("0x0100") +##BIDENT2 = b("\x01\x00") +UIDENT = u("0x0100") + +def _ident_mssql(hash, csize, bsize): + "common identify for mssql 2000/2005" + if isinstance(hash, unicode): + if len(hash) == csize and hash.startswith(UIDENT): + return True + elif isinstance(hash, bytes): + if len(hash) == csize and hash.startswith(BIDENT): + return True + ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes + ## return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + return False + +def _parse_mssql(hash, csize, bsize, handler): + "common parser for mssql 2000/2005; returns 4 byte salt + checksum" + if isinstance(hash, unicode): + if len(hash) == csize and hash.startswith(UIDENT): + try: + return unhexlify(hash[6:].encode("utf-8")) + except TypeError: # throw when bad char found + pass + elif isinstance(hash, bytes): + # assumes ascii-compat encoding + assert isinstance(hash, bytes) + if len(hash) == csize and hash.startswith(BIDENT): + try: + return unhexlify(hash[6:]) + except TypeError: # throw when bad char found + pass + ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes + ## return hash[2:] + else: + raise uh.exc.ExpectedStringError(hash, "hash") + raise uh.exc.InvalidHashError(handler) + +class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the password hash used by MS-SQL 2000, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 bytes in length. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "mssql2000" + setting_kwds = ("salt",) + checksum_size = 40 + min_salt_size = max_salt_size = 4 + _stub_checksum = b("\x00") * 40 + + #=================================================================== + # formatting + #=================================================================== + + # 0100 - 2 byte identifier + # 4 byte salt + # 20 byte checksum + # 20 byte checksum + # = 46 bytes + # encoded '0x' + 92 chars = 94 + + @classmethod + def identify(cls, hash): + return _ident_mssql(hash, 94, 46) + + @classmethod + def from_string(cls, hash): + data = _parse_mssql(hash, 94, 46, cls) + return cls(salt=data[:4], checksum=data[4:]) + + def to_string(self): + raw = self.salt + (self.checksum or self._stub_checksum) + # raw bytes format - BIDENT2 + raw + return "0x0100" + bascii_to_str(hexlify(raw).upper()) + + def _calc_checksum(self, secret): + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + salt = self.salt + return _raw_mssql(secret, salt) + _raw_mssql(secret.upper(), salt) + + @classmethod + def verify(cls, secret, hash): + # NOTE: we only compare against the upper-case hash + # XXX: add 'full' just to verify both checksums? + uh.validate_secret(secret) + self = cls.from_string(hash) + chk = self.checksum + if chk is None: + raise uh.exc.MissingDigestError(cls) + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + result = _raw_mssql(secret.upper(), self.salt) + return consteq(result, chk[20:]) + +#============================================================================= +# handler +#============================================================================= +class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the password hash used by MS-SQL 2005, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 bytes in length. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "mssql2005" + setting_kwds = ("salt",) + + checksum_size = 20 + min_salt_size = max_salt_size = 4 + _stub_checksum = b("\x00") * 20 + + #=================================================================== + # formatting + #=================================================================== + + # 0x0100 - 2 byte identifier + # 4 byte salt + # 20 byte checksum + # = 26 bytes + # encoded '0x' + 52 chars = 54 + + @classmethod + def identify(cls, hash): + return _ident_mssql(hash, 54, 26) + + @classmethod + def from_string(cls, hash): + data = _parse_mssql(hash, 54, 26, cls) + return cls(salt=data[:4], checksum=data[4:]) + + def to_string(self): + raw = self.salt + (self.checksum or self._stub_checksum) + # raw bytes format - BIDENT2 + raw + return "0x0100" + bascii_to_str(hexlify(raw)).upper() + + def _calc_checksum(self, secret): + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + return _raw_mssql(secret, self.salt) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/mysql.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,128 @@ +"""passlib.handlers.mysql + +MySQL 3.2.3 / OLD_PASSWORD() + + This implements Mysql's OLD_PASSWORD algorithm, introduced in version 3.2.3, deprecated in version 4.1. + + See :mod:`passlib.handlers.mysql_41` for the new algorithm was put in place in version 4.1 + + This algorithm is known to be very insecure, and should only be used to verify existing password hashes. + + http://djangosnippets.org/snippets/1508/ + +MySQL 4.1.1 / NEW PASSWORD + This implements Mysql new PASSWORD algorithm, introduced in version 4.1. + + This function is unsalted, and therefore not very secure against rainbow attacks. + It should only be used when dealing with mysql passwords, + for all other purposes, you should use a salted hash function. + + Description taken from http://dev.mysql.com/doc/refman/6.0/en/password-hashing.html +""" +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import sha1 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_native_str +from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, \ + byte_elem_value, str_to_uascii +import passlib.utils.handlers as uh +# local +__all__ = [ + 'mysql323', + 'mysq41', +] + +#============================================================================= +# backend +#============================================================================= +class mysql323(uh.StaticHandler): + """This class implements the MySQL 3.2.3 password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "mysql323" + checksum_size = 16 + checksum_chars = uh.HEX_CHARS + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + # FIXME: no idea if mysql has a policy about handling unicode passwords + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + + MASK_32 = 0xffffffff + MASK_31 = 0x7fffffff + WHITE = b(' \t') + + nr1 = 0x50305735 + nr2 = 0x12345671 + add = 7 + for c in secret: + if c in WHITE: + continue + tmp = byte_elem_value(c) + nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32 + nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32 + add = (add+tmp) & MASK_32 + return u("%08x%08x") % (nr1 & MASK_31, nr2 & MASK_31) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# handler +#============================================================================= +class mysql41(uh.StaticHandler): + """This class implements the MySQL 4.1 password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "mysql41" + _hash_prefix = u("*") + checksum_chars = uh.HEX_CHARS + checksum_size = 40 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.upper() + + def _calc_checksum(self, secret): + # FIXME: no idea if mysql has a policy about handling unicode passwords + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(sha1(sha1(secret).digest()).hexdigest()).upper() + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/oracle.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,175 @@ +"""passlib.handlers.oracle - Oracle DB Password Hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import sha1 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_unicode, to_native_str, xor_bytes +from passlib.utils.compat import b, bytes, bascii_to_str, irange, u, \ + uascii_to_str, unicode, str_to_uascii +from passlib.utils.des import des_encrypt_block +import passlib.utils.handlers as uh +# local +__all__ = [ + "oracle10g", + "oracle11g" +] + +#============================================================================= +# oracle10 +#============================================================================= +def des_cbc_encrypt(key, value, iv=b('\x00') * 8, pad=b('\x00')): + """performs des-cbc encryption, returns only last block. + + this performs a specific DES-CBC encryption implementation + as needed by the Oracle10 hash. it probably won't be useful for + other purposes as-is. + + input value is null-padded to multiple of 8 bytes. + + :arg key: des key as bytes + :arg value: value to encrypt, as bytes. + :param iv: optional IV + :param pad: optional pad byte + + :returns: last block of DES-CBC encryption of all ``value``'s byte blocks. + """ + value += pad * (-len(value) % 8) # null pad to multiple of 8 + hash = iv # start things off + for offset in irange(0,len(value),8): + chunk = xor_bytes(hash, value[offset:offset+8]) + hash = des_encrypt_block(key, chunk) + return hash + +# magic string used as initial des key by oracle10 +ORACLE10_MAGIC = b("\x01\x23\x45\x67\x89\xAB\xCD\xEF") + +class oracle10(uh.HasUserContext, uh.StaticHandler): + """This class implements the password hash used by Oracle up to version 10g, and follows the :ref:`password-hash-api`. + + It does a single round of hashing, and relies on the username as the salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keywords: + + :type user: str + :param user: name of oracle user account this password is associated with. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "oracle10" + checksum_chars = uh.HEX_CHARS + checksum_size = 16 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.upper() + + def _calc_checksum(self, secret): + # FIXME: not sure how oracle handles unicode. + # online docs about 10g hash indicate it puts ascii chars + # in a 2-byte encoding w/ the high byte set to null. + # they don't say how it handles other chars, or what encoding. + # + # so for now, encoding secret & user to utf-16-be, + # since that fits, and if secret/user is bytes, + # we assume utf-8, and decode first. + # + # this whole mess really needs someone w/ an oracle system, + # and some answers :) + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + user = to_unicode(self.user, "utf-8", param="user") + input = (user+secret).upper().encode("utf-16-be") + hash = des_cbc_encrypt(ORACLE10_MAGIC, input) + hash = des_cbc_encrypt(hash, input) + return hexlify(hash).decode("ascii").upper() + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# oracle11 +#============================================================================= +class oracle11(uh.HasSalt, uh.GenericHandler): + """This class implements the Oracle11g password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 20 hexidecimal characters. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "oracle11" + setting_kwds = ("salt",) + checksum_size = 40 + checksum_chars = uh.UPPER_HEX_CHARS + + _stub_checksum = u('0') * 40 + + #--HasSalt-- + min_salt_size = max_salt_size = 20 + salt_chars = uh.UPPER_HEX_CHARS + + + #=================================================================== + # methods + #=================================================================== + _hash_regex = re.compile(u("^S:(?P<chk>[0-9a-f]{40})(?P<salt>[0-9a-f]{20})$"), re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + salt, chk = m.group("salt", "chk") + return cls(salt=salt, checksum=chk.upper()) + + def to_string(self): + chk = (self.checksum or self._stub_checksum) + hash = u("S:%s%s") % (chk.upper(), self.salt.upper()) + return uascii_to_str(hash) + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + chk = sha1(secret + unhexlify(self.salt.encode("ascii"))).hexdigest() + return str_to_uascii(chk).upper() + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/pbkdf2.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,488 @@ +"""passlib.handlers.pbkdf - PBKDF2 based hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from base64 import b64encode, b64decode +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import ab64_decode, ab64_encode, to_unicode +from passlib.utils.compat import b, bytes, str_to_bascii, u, uascii_to_str, unicode +from passlib.utils.pbkdf2 import pbkdf2 +import passlib.utils.handlers as uh +# local +__all__ = [ + "pbkdf2_sha1", + "pbkdf2_sha256", + "pbkdf2_sha512", + "cta_pbkdf2_sha1", + "dlitz_pbkdf2_sha1", + "grub_pbkdf2_sha512", +] + +#============================================================================= +# +#============================================================================= +class Pbkdf2DigestHandler(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + "base class for various pbkdf2_{digest} algorithms" + #=================================================================== + # class attrs + #=================================================================== + + #--GenericHandler-- + setting_kwds = ("salt", "salt_size", "rounds") + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + default_salt_size = 16 + min_salt_size = 0 + max_salt_size = 1024 + + #--HasRounds-- + default_rounds = None # set by subclass + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + #--this class-- + _prf = None # subclass specified prf identifier + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check. + # the underlying pbkdf2 specifies no bounds for either. + + # NOTE: defaults chosen to be at least as large as pbkdf2 rfc recommends... + # >8 bytes of entropy in salt, >1000 rounds + # increased due to time since rfc established + + #=================================================================== + # methods + #=================================================================== + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + salt = ab64_decode(salt.encode("ascii")) + if chk: + chk = ab64_decode(chk.encode("ascii")) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self, withchk=True): + salt = ab64_encode(self.salt).decode("ascii") + if withchk and self.checksum: + chk = ab64_encode(self.checksum).decode("ascii") + else: + chk = None + return uh.render_mc3(self.ident, self.rounds, salt, chk) + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return pbkdf2(secret, self.salt, self.rounds, self.checksum_size, self._prf) + +def create_pbkdf2_hash(hash_name, digest_size, rounds=12000, ident=None, module=__name__): + "create new Pbkdf2DigestHandler subclass for a specific hash" + name = 'pbkdf2_' + hash_name + if ident is None: + ident = u("$pbkdf2-%s$") % (hash_name,) + prf = "hmac-%s" % (hash_name,) + base = Pbkdf2DigestHandler + return type(name, (base,), dict( + __module__=module, # so ABCMeta won't clobber it. + name=name, + ident=ident, + _prf = prf, + default_rounds=rounds, + checksum_size=digest_size, + encoded_checksum_size=(digest_size*4+2)//3, + __doc__="""This class implements a generic ``PBKDF2-%(prf)s``-based password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, the length must be between 0-1024 bytes. + If not specified, a %(dsc)d byte salt will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to %(dr)d, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ % dict(prf=prf.upper(), dsc=base.default_salt_size, dr=rounds) + )) + +#------------------------------------------------------------------------ +# derived handlers +#------------------------------------------------------------------------ +pbkdf2_sha1 = create_pbkdf2_hash("sha1", 20, 60000, ident=u("$pbkdf2$")) +pbkdf2_sha256 = create_pbkdf2_hash("sha256", 32) +pbkdf2_sha512 = create_pbkdf2_hash("sha512", 64) + +ldap_pbkdf2_sha1 = uh.PrefixWrapper("ldap_pbkdf2_sha1", pbkdf2_sha1, "{PBKDF2}", "$pbkdf2$", ident=True) +ldap_pbkdf2_sha256 = uh.PrefixWrapper("ldap_pbkdf2_sha256", pbkdf2_sha256, "{PBKDF2-SHA256}", "$pbkdf2-sha256$", ident=True) +ldap_pbkdf2_sha512 = uh.PrefixWrapper("ldap_pbkdf2_sha512", pbkdf2_sha512, "{PBKDF2-SHA512}", "$pbkdf2-sha512$", ident=True) + +#============================================================================= +# cryptacular's pbkdf2 hash +#============================================================================= + +# bytes used by cta hash for base64 values 63 & 64 +CTA_ALTCHARS = b("-_") + +class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements Cryptacular's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, it may be any length. + If not specified, a one will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 60000, must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "cta_pbkdf2_sha1" + setting_kwds = ("salt", "salt_size", "rounds") + ident = u("$p5k2$") + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. underlying algorithm (and reference implementation) + # allows effectively unbounded values for both of these parameters. + + #--HasSalt-- + default_salt_size = 16 + min_salt_size = 0 + max_salt_size = 1024 + + #--HasRounds-- + default_rounds = 60000 + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + #=================================================================== + # formatting + #=================================================================== + + # hash $p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0= + # ident $p5k2$ + # rounds 1000 + # salt ZxK4ZBJCfQg= + # chk jJZVscWtO--p1-xIZl6jhO2LKR0= + # NOTE: rounds in hex + + @classmethod + def from_string(cls, hash): + # NOTE: passlib deviation - forbidding zero-padded rounds + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16, handler=cls) + salt = b64decode(salt.encode("ascii"), CTA_ALTCHARS) + if chk: + chk = b64decode(chk.encode("ascii"), CTA_ALTCHARS) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self, withchk=True): + salt = b64encode(self.salt, CTA_ALTCHARS).decode("ascii") + if withchk and self.checksum: + chk = b64encode(self.checksum, CTA_ALTCHARS).decode("ascii") + else: + chk = None + return uh.render_mc3(self.ident, self.rounds, salt, chk, rounds_base=16) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return pbkdf2(secret, self.salt, self.rounds, 20, "hmac-sha1") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# dlitz's pbkdf2 hash +#============================================================================= +class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements Dwayne Litzenberger's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If specified, it may be any length, but must use the characters in the regexp range ``[./0-9A-Za-z]``. + If not specified, a 16 character salt will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 60000, must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "dlitz_pbkdf2_sha1" + setting_kwds = ("salt", "salt_size", "rounds") + ident = u("$p5k2$") + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. underlying algorithm (and reference implementation) + # allows effectively unbounded values for both of these parameters. + + #--HasSalt-- + default_salt_size = 16 + min_salt_size = 0 + max_salt_size = 1024 + salt_chars = uh.HASH64_CHARS + + #--HasRounds-- + default_rounds = 60000 + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + #=================================================================== + # formatting + #=================================================================== + + # hash $p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + # ident $p5k2$ + # rounds c + # salt u9HvcT4d + # chk Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + # rounds in lowercase hex, no zero padding + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16, + default_rounds=400, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self, withchk=True): + rounds = self.rounds + if rounds == 400: + rounds = None # omit rounds measurement if == 400 + return uh.render_mc3(self.ident, rounds, self.salt, + checksum=self.checksum if withchk else None, + rounds_base=16) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + salt = str_to_bascii(self.to_string(withchk=False)) + result = pbkdf2(secret, salt, self.rounds, 24, "hmac-sha1") + return ab64_encode(result).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# crowd +#============================================================================= +class atlassian_pbkdf2_sha1(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the PBKDF2 hash used by Atlassian. + + It supports a fixed-length salt, and a fixed number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, the length must be exactly 16 bytes. + If not specified, a salt will be autogenerated (this is recommended). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #--GenericHandler-- + name = "atlassian_pbkdf2_sha1" + setting_kwds =("salt",) + ident = u("{PKCS5S2}") + checksum_size = 32 + + _stub_checksum = b("\x00") * 32 + + #--HasRawSalt-- + min_salt_size = max_salt_size = 16 + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + ident = cls.ident + if not hash.startswith(ident): + raise uh.exc.InvalidHashError(cls) + data = b64decode(hash[len(ident):].encode("ascii")) + salt, chk = data[:16], data[16:] + return cls(salt=salt, checksum=chk) + + def to_string(self): + data = self.salt + (self.checksum or self._stub_checksum) + hash = self.ident + b64encode(data).decode("ascii") + return uascii_to_str(hash) + + def _calc_checksum(self, secret): + # TODO: find out what crowd's policy is re: unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + # crowd seems to use a fixed number of rounds. + return pbkdf2(secret, self.salt, 10000, 32, "hmac-sha1") + +#============================================================================= +# grub +#============================================================================= +class grub_pbkdf2_sha512(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements Grub's pbkdf2-hmac-sha512 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, the length must be between 0-1024 bytes. + If not specified, a 64 byte salt will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 64 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 12000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + name = "grub_pbkdf2_sha512" + setting_kwds = ("salt", "salt_size", "rounds") + + ident = u("grub.pbkdf2.sha512.") + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. the underlying pbkdf2 specifies no bounds for either, + # and it's not clear what grub specifies. + + default_salt_size = 64 + min_salt_size = 0 + max_salt_size = 1024 + + default_rounds = 12000 + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, sep=u("."), + handler=cls) + salt = unhexlify(salt.encode("ascii")) + if chk: + chk = unhexlify(chk.encode("ascii")) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self, withchk=True): + salt = hexlify(self.salt).decode("ascii").upper() + if withchk and self.checksum: + chk = hexlify(self.checksum).decode("ascii").upper() + else: + chk = None + return uh.render_mc3(self.ident, self.rounds, salt, chk, sep=u(".")) + + def _calc_checksum(self, secret): + # TODO: find out what grub's policy is re: unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return pbkdf2(secret, self.salt, self.rounds, 64, "hmac-sha512") + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/phpass.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,137 @@ +"""passlib.handlers.phpass - PHPass Portable Crypt + +phppass located - http://www.openwall.com/phpass/ +algorithm described - http://www.openwall.com/articles/PHP-Users-Passwords + +phpass context - blowfish, bsdi_crypt, phpass +""" +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import md5 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import h64 +from passlib.utils.compat import b, bytes, u, uascii_to_str, unicode +import passlib.utils.handlers as uh +# local +__all__ = [ + "phpass", +] + +#============================================================================= +# phpass +#============================================================================= +class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements the PHPass Portable Hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 16, must be between 7 and 30, inclusive. + This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`. + + :type ident: str + :param ident: + phpBB3 uses ``H`` instead of ``P`` for it's identifier, + this may be set to ``H`` in order to generate phpBB3 compatible hashes. + it defaults to ``P``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "phpass" + setting_kwds = ("salt", "rounds", "ident") + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + min_salt_size = max_salt_size = 8 + salt_chars = uh.HASH64_CHARS + + #--HasRounds-- + default_rounds = 16 + min_rounds = 7 + max_rounds = 30 + rounds_cost = "log2" + + #--HasManyIdents-- + default_ident = u("$P$") + ident_values = [u("$P$"), u("$H$")] + ident_aliases = {u("P"):u("$P$"), u("H"):u("$H$")} + + #=================================================================== + # formatting + #=================================================================== + + #$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0 + # $P$ + # 9 + # IQRaTwmf + # eRo7ud9Fh4E2PdI0S3r.L0 + + @classmethod + def from_string(cls, hash): + ident, data = cls._parse_ident(hash) + rounds, salt, chk = data[0], data[1:9], data[9:] + return cls( + ident=ident, + rounds=h64.decode_int6(rounds.encode("ascii")), + salt=salt, + checksum=chk or None, + ) + + def to_string(self): + hash = u("%s%s%s%s") % (self.ident, + h64.encode_int6(self.rounds).decode("ascii"), + self.salt, + self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + # FIXME: can't find definitive policy on how phpass handles non-ascii. + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + real_rounds = 1<<self.rounds + result = md5(self.salt.encode("ascii") + secret).digest() + r = 0 + while r < real_rounds: + result = md5(result + secret).digest() + r += 1 + return h64.encode_bytes(result).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/postgres.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,57 @@ +"""passlib.handlers.postgres_md5 - MD5-based algorithm used by Postgres for pg_shadow table""" +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import md5 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_bytes +from passlib.utils.compat import b, bytes, str_to_uascii, unicode, u +import passlib.utils.handlers as uh +# local +__all__ = [ + "postgres_md5", +] + +#============================================================================= +# handler +#============================================================================= +class postgres_md5(uh.HasUserContext, uh.StaticHandler): + """This class implements the Postgres MD5 Password hash, and follows the :ref:`password-hash-api`. + + It does a single round of hashing, and relies on the username as the salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keywords: + + :type user: str + :param user: name of postgres user account this password is associated with. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "postgres_md5" + _hash_prefix = u("md5") + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + #=================================================================== + # primary interface + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + user = to_bytes(self.user, "utf-8", param="user") + return str_to_uascii(md5(secret + user).hexdigest()) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/roundup.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,29 @@ +"""passlib.handlers.roundup - Roundup issue tracker hashes""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +# site +# pkg +import passlib.utils.handlers as uh +from passlib.utils.compat import u +# local +__all__ = [ + "roundup_plaintext", + "ldap_hex_md5", + "ldap_hex_sha1", +] +#============================================================================= +# +#============================================================================= +roundup_plaintext = uh.PrefixWrapper("roundup_plaintext", "plaintext", + prefix=u("{plaintext}"), lazy=True) + +# NOTE: these are here because they're currently only known to be used by roundup +ldap_hex_md5 = uh.PrefixWrapper("ldap_hex_md5", "hex_md5", u("{MD5}"), lazy=True) +ldap_hex_sha1 = uh.PrefixWrapper("ldap_hex_sha1", "hex_sha1", u("{SHA}"), lazy=True) + +#============================================================================= +# eof +#=============================================================================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/passlib/handlers/scram.py Fri Jan 18 01:38:07 2013 +0100 @@ -0,0 +1,576 @@ +"""passlib.handlers.scram - hash for SCRAM credential storage""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from base64 import b64encode, b64decode +import hashlib +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.exc import PasslibHashWarning +from passlib.utils import ab64_decode, ab64_encode, consteq, saslprep, \ + to_native_str, xor_bytes, splitcomma +from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, \ + PY3, u, unicode +from passlib.utils.pbkdf2 import pbkdf2, get_prf, norm_hash_name +import passlib.utils.handlers as uh +# local +__all__ = [ + "scram", +] + +#============================================================================= +# scram credentials hash +#============================================================================= +class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class provides a format for storing SCRAM passwords, and follows + the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, the length must be between 0-1024 bytes. + If not specified, a 12 byte salt will be autogenerated + (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 12 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 6400, but must be within ``range(1,1<<32)``. + + :type algs: list of strings + :param algs: + Specify list of digest algorithms to use. + + By default each scram hash will contain digests for SHA-1, + SHA-256, and SHA-512. This can be overridden by specify either be a + list such as ``["sha-1", "sha-256"]``, or a comma-separated string + such as ``"sha-1, sha-256"``. Names are case insensitive, and may + use :mod:`!hashlib` or `IANA <http://www.iana.org/assignments/hash-function-text-names>`_ + hash names. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + In addition to the standard :ref:`password-hash-api` methods, + this class also provides the following methods for manipulating Passlib + scram hashes in ways useful for pluging into a SCRAM protocol stack: + + .. automethod:: extract_digest_info + .. automethod:: extract_digest_algs + .. automethod:: derive_digest + """ + #=================================================================== + # class attrs + #=================================================================== + + # NOTE: unlike most GenericHandler classes, the 'checksum' attr of + # ScramHandler is actually a map from digest_name -> digest, so + # many of the standard methods have been overridden. + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide + # a sanity check; the underlying pbkdf2 specifies no bounds for either. + + #--GenericHandler-- + name = "scram" + setting_kwds = ("salt", "salt_size", "rounds", "algs") + ident = u("$scram$") + + #--HasSalt-- + default_salt_size = 12 + min_salt_size = 0 + max_salt_size = 1024 + + #--HasRounds-- + default_rounds = 6400 + min_rounds = 1 + max_rounds = 2**32-1 + rounds_cost = "linear" + + #--custom-- + + # default algorithms when creating new hashes. + default_algs = ["sha-1", "sha-256", "sha-512"] + + # list of algs verify prefers to use, in order. + _verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"] + + #=================================================================== + # instance attrs + #=================================================================== + + # 'checksum' is different from most GenericHandler subclasses, + # in that it contains a dict mapping from alg -> digest, + # or None if no checksum present. + + # list of algorithms to create/compare digests for. + algs = None + + #=================================================================== + # scram frontend helpers + #=================================================================== + @classmethod + def extract_digest_info(cls, hash, alg): + """return (salt, rounds, digest) for specific hash algorithm. + + :type hash: str + :arg hash: + :class:`!scram` hash stored for desired user + + :type alg: str + :arg alg: + Name of digest algorithm (e.g. ``"sha-1"``) requested by client. + + This value is run through :func:`~passlib.utils.pbkdf2.norm_hash_name`, + so it is case-insensitive, and can be the raw SCRAM + mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name, + or the hashlib name. + + :raises KeyError: + If the hash does not contain an entry for the requested digest + algorithm. + + :returns: + A tuple containing ``(salt, rounds, digest)``, + where *digest* matches the raw bytes returned by + SCRAM's :func:`Hi` function for the stored password, + the provided *salt*, and the iteration count (*rounds*). + *salt* and *digest* are both raw (unencoded) bytes. + """ + # XXX: this could be sped up by writing custom parsing routine + # that just picks out relevant digest, and doesn't bother + # with full structure validation each time it's called. + alg = norm_hash_name(alg, 'iana') + self = cls.from_string(hash) + chkmap = self.checksum + if not chkmap: + raise ValueError("scram hash contains no digests") + return self.salt, self.rounds, chkmap[alg] + + @classmethod + def extract_digest_algs(cls, hash, format="iana"): + """Return names of all algorithms stored in a given hash. + + :type hash: str + :arg hash: + The :class:`!scram` hash to parse + + :type format: str + :param format: + This changes the naming convention used by the + returned algorithm names. By default the names + are IANA-compatible; see :func:`~passlib.utils.pbkdf2.norm_hash_name` + for possible values. + + :returns: + Returns a list of digest algorithms; e.g. ``["sha-1"]`` + """ + # XXX: this could be sped up by writing custom parsing routine + # that just picks out relevant names, and doesn't bother + # with full structure validation each time it's called. + algs = cls.from_string(hash).algs + if format == "iana": + return algs + else: + return [norm_hash_name(alg, format) for alg in algs] + + @classmethod + def derive_digest(cls, password, salt, rounds, alg): + """helper to create SaltedPassword digest for SCRAM. + + This performs the step in the SCRAM protocol described as:: + + SaltedPassword := Hi(Normalize(password), salt, i) + + :type password: unicode or utf-8 bytes + :arg password: password to run through digest + + :type salt: bytes + :arg salt: raw salt data + + :type rounds: int + :arg rounds: number of iterations. + + :type alg: str + :arg alg: name of digest to use (e.g. ``"sha-1"``). + + :returns: + raw bytes of ``SaltedPassword`` + """ + if isinstance(password, bytes): + password = password.decode("utf-8") + password = saslprep(password).encode("utf-8") + if not isinstance(salt, bytes): + raise TypeError("salt must be bytes") + if rounds < 1: + raise ValueError("rounds must be >= 1") + alg = norm_hash_name(alg, "hashlib") + return pbkdf2(password, salt, rounds, None, "hmac-" + alg) + + #=================================================================== + # serialization + #=================================================================== + + @classmethod + def from_string(cls, hash): + hash = to_native_str(hash, "ascii", "hash") + if not hash.startswith("$scram$"): + raise uh.exc.InvalidHashError(cls) + parts = hash[7:].split("$") + if len(parts) != 3: + raise uh.exc.MalformedHashError(cls) + rounds_str, salt_str, chk_str = parts + + # decode rounds + rounds = int(rounds_str) + if rounds_str != str(rounds): # forbid zero padding, etc. + raise uh.exc.MalformedHashError(cls) + + # decode salt + try: + salt = ab64_decode(salt_str.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + + # decode algs/digest list + if not chk_str: + # scram hashes MUST have something here. + raise uh.exc.MalformedHashError(cls) + elif "=" in chk_str: + # comma-separated list of 'alg=digest' pairs + algs = None + chkmap = {} + for pair in chk_str.split(","): + alg, digest = pair.split("=") + try: + chkmap[alg] = ab64_decode(digest.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + else: + # comma-separated list of alg names, no digests + algs = chk_str + chkmap = None + + # return new object + return cls( + rounds=rounds, + salt=salt, + checksum=chkmap, + algs=algs, + ) + + def to_string(self, withchk=True): + salt = bascii_to_str(ab64_encode(self.salt)) + chkmap = self.checksum + if withchk and chkmap: + chk_str = ",".join( + "%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg]))) + for alg in self.algs + ) + else: + chk_str = ",".join(self.algs) + return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str) + + #=================================================================== + # init + #=================================================================== + def __init__(self, algs=None, **kwds): + super(scram, self).__init__(**kwds) + self.algs = self._norm_algs(algs) + + def _norm_checksum(self, checksum): + if checksum is None: + return None + for alg, digest in iteritems(checksum): + if alg != norm_hash_name(alg, 'iana'): + raise ValueError("malformed algorithm name in scram hash: %r" % + (alg,)) + if len(alg) > 9: + raise ValueError("SCRAM limits algorithm names to " + "9 characters: %r" % (alg,)) + if not isinstance(digest, bytes): + raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests") + # TODO: verify digest size (if digest is known) + if 'sha-1' not in checksum: + # NOTE: required because of SCRAM spec. + raise ValueError("sha-1 must be in algorithm list of scram hash") + return checksum + + def _norm_algs(self, algs): + "normalize algs parameter" + # determine default algs value + if algs is None: + # derive algs list from checksum (if present). + chk = self.checksum + if chk is not None: + return sorted(chk) + elif self.use_defaults: + return list(self.default_algs) + else: + raise TypeError("no algs list specified") + elif self.checksum is not None: + raise RuntimeError("checksum & algs kwds are mutually exclusive") + + # parse args value + if isinstance(algs, str): + algs = splitcomma(algs) + algs = sorted(norm_hash_name(alg, 'iana') for alg in algs) + if any(len(alg)>9 for alg in algs): + raise ValueError("SCRAM limits alg names to max of 9 characters") + if 'sha-1' not in algs: + # NOTE: required because of SCRAM spec (rfc 5802) + raise ValueError("sha-1 must be in algorithm list of scram hash") + return algs + + #=================================================================== + # digest methods + #=================================================================== + + @classmethod + def _bind_needs_update(cls, **settings): + "generate a deprecation detector for CryptContext to use" + # generate deprecation hook which marks hashes as deprecated + # if they don't support a superset of current algs. + algs = frozenset(cls(use_defaults=True, **settings).algs) + def detector(hash, secret): + return not algs.issubset(cls.from_string(hash).algs) + return detector + + def _calc_checksum(self, secret, alg=None): + rounds = self.rounds + salt = self.salt + hash = self.derive_digest + if alg: + # if requested, generate digest for specific alg + return hash(secret, salt, rounds, alg) + else: + # by default, return dict containing digests for all algs + return dict( + (alg, hash(secret, salt, rounds, alg)) + for alg in self.algs + ) + + @classmethod + def verify(cls, secret, hash, full=False): + uh.validate_secret(secret) + self = cls.from_string(hash) + chkmap = self.checksum + if not chkmap: + raise ValueError("expected %s hash, got %s config string instead" % + (cls.name, cls.name)) + + # NOTE: to make the verify method efficient, we just calculate hash + # of shortest digest by default. apps can pass in "full=True" to + # check entire hash for consistency. + if full: + correct = failed = False + for alg, digest in iteritems(chkmap): + other = self._calc_checksum(secret, alg) + # NOTE: could do this length check in norm_algs(), + # but don't need to be that strict, and want to be able + # to parse hashes containing algs not supported by platform. + # it's fine if we fail here though. + if len(digest) != len(other): + raise ValueError("mis-sized %s digest in scram hash: %r != %r" + % (alg, len(digest), len(other))) + if consteq(other, digest): + correct = True + else: + failed = True + if correct and failed: + raise ValueError("scram hash verified inconsistently, " + "may be corrupted") + else: + return correct + else: + # XXX: should this just always use sha1 hash? would be faster. + # otherwise only verify against one hash, pick one w/ best security. + for alg in self._verify_algs: + if alg in chkmap: + other = self._calc_checksum(secret, alg) + return consteq(other, chkmap[alg]) + # there should always be sha-1 at the very least, + # or something went wrong inside _norm_algs() + raise AssertionError("sha-1 digest not found!") + + #=================================================================== + # + #=================================================================== + +#============================================================================= +# code used for testing scram against protocol examples during development. +#============================================================================= +##def _test_reference_scram(): +## "quick hack testing scram reference vectors" +## # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801 +## from passlib.utils.compat import print_ +## +## engine = _scram_engine( +## alg="sha-1", +## salt='QSXCR+Q6sek8bf92'.decode("base64"), +## rounds=4096, +## password=u("pencil"), +## ) +## print_(engine.digest.encode("base64").rstrip()) +## +## msg = engine.format_auth_msg( +## username="user", +## client_nonce = "fyko+d2lbbFgONRv9qkxdawL", +## server_nonce = "3rfcNHYJY1ZVvWVs7j", +## header='c=biws', +## ) +## +## cp = engine.get_encoded_client_proof(msg) +## assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp +## +## ss = engine.get_encoded_server_sig(msg) +## assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss +## +##class _scram_engine(object): +## """helper class for verifying scram hash behavior +## against SCRAM protocol examples. not officially part of Passlib. +## +## takes in alg, salt, rounds, and a digest or password. +## +## can calculate the various keys & messages of the scram protocol. +## +## """ +## #========================================================= +## # init +## #========================================================= +## +## @classmethod +## def from_string(cls, hash, alg): +## "create record from scram hash, for given alg" +## return cls(alg, *scram.extract_digest_info(hash, alg)) +## +## def __init__(self, alg, salt, rounds, digest=None, password=None): +## self.alg = norm_hash_name(alg) +## self.salt = salt +## self.rounds = rounds +## self.password = password +## if password: +## data = scram.derive_digest(password, salt, rounds, alg) +## if digest and data != digest: +## raise ValueError("password doesn't match digest") +## else: +## digest = data +## elif not digest: +## raise TypeError("must provide password or digest") +## self.digest = digest +## +## #========================================================= +## # frontend methods +## #========================================================= +## def get_hash(self, data): +## "return hash of raw data" +## return hashlib.new(iana_to_hashlib(self.alg), data).digest() +## +## def get_client_proof(self, msg): +## "return client proof of specified auth msg text" +## return xor_bytes(self.client_key, self.get_client_sig(msg)) +## +## def get_encoded_client_proof(self, msg): +## return self.get_client_proof(msg).encode("base64").rstrip() +## +## def get_client_sig(self, msg): +## "return client signature of specified auth msg text" +## return self.get_hmac(self.stored_key, msg) +## +## def get_server_sig(self, msg): +## "return server signature of specified auth msg text" +## return self.get_hmac(self.server_key, msg) +## +## def get_encoded_server_sig(self, msg): +## return self.get_server_sig(msg).encode("base64").rstrip() +## +## def format_server_response(self, client_nonce, server_nonce): +## return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format( +## client_nonce=client_nonce, +## server_nonce=server_nonce, +## rounds=self.rounds, +## salt=self.encoded_salt, +## ) +## +## def format_auth_msg(self, username, client_nonce, server_nonce, +## header='c=biws'): +## return ( +## 'n={username},r={client_nonce}' +## ',' +## 'r={client_nonce}{server_nonce},s={salt},i={rounds}' +## ',' +## '{header},r={client_nonce}{server_nonce}' +## ).format( +## username=username, +## client_nonce=client_nonce, +## server_nonce=server_nonce, +## salt=self.encoded_salt, +## rounds=self.rounds, +## header=header, +## ) +## +## #========================================================= +## # helpers to calculate & cache constant data +## #=====