| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- """passlib.registry - registry for password hash handlers"""
- #=============================================================================
- # imports
- #=============================================================================
- # core
- import re
- import logging; log = logging.getLogger(__name__)
- from warnings import warn
- # pkg
- from passlib import exc
- from passlib.exc import ExpectedTypeError, PasslibWarning
- from passlib.ifc import PasswordHash
- from passlib.utils import (
- is_crypt_handler, has_crypt as os_crypt_present,
- unix_crypt_schemes as os_crypt_schemes,
- )
- from passlib.utils.compat import unicode_or_str
- from passlib.utils.decor import memoize_single_value
- # local
- __all__ = [
- "register_crypt_handler_path",
- "register_crypt_handler",
- "get_crypt_handler",
- "list_crypt_handlers",
- ]
- #=============================================================================
- # proxy object used in place of 'passlib.hash' module
- #=============================================================================
- class _PasslibRegistryProxy(object):
- """proxy module passlib.hash
- this module is in fact an object which lazy-loads
- the requested password hash algorithm from wherever it has been stored.
- it acts as a thin wrapper around :func:`passlib.registry.get_crypt_handler`.
- """
- __name__ = "passlib.hash"
- __package__ = None
- def __getattr__(self, attr):
- if attr.startswith("_"):
- raise AttributeError("missing attribute: %r" % (attr,))
- handler = get_crypt_handler(attr, None)
- if handler:
- return handler
- else:
- raise AttributeError("unknown password hash: %r" % (attr,))
- def __setattr__(self, attr, value):
- if attr.startswith("_"):
- # writing to private attributes should behave normally.
- # (required so GAE can write to the __loader__ attribute).
- object.__setattr__(self, attr, value)
- else:
- # writing to public attributes should be treated
- # as attempting to register a handler.
- register_crypt_handler(value, _attr=attr)
- def __repr__(self):
- return "<proxy module 'passlib.hash'>"
- def __dir__(self):
- # this adds in lazy-loaded handler names,
- # otherwise this is the standard dir() implementation.
- attrs = set(dir(self.__class__))
- attrs.update(self.__dict__)
- attrs.update(_locations)
- return sorted(attrs)
- # create single instance - available publically as 'passlib.hash'
- _proxy = _PasslibRegistryProxy()
- #=============================================================================
- # internal registry state
- #=============================================================================
- # singleton uses to detect omitted keywords
- _UNSET = object()
- # dict mapping name -> loaded handlers (just uses proxy object's internal dict)
- _handlers = _proxy.__dict__
- # dict mapping names -> import path for lazy loading.
- # * import path should be "module.path" or "module.path:attr"
- # * if attr omitted, "name" used as default.
- _locations = dict(
- # NOTE: this is a hardcoded list of the handlers built into passlib,
- # applications should call register_crypt_handler_path()
- apr_md5_crypt = "passlib.handlers.md5_crypt",
- argon2 = "passlib.handlers.argon2",
- atlassian_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
- bcrypt = "passlib.handlers.bcrypt",
- bcrypt_sha256 = "passlib.handlers.bcrypt",
- bigcrypt = "passlib.handlers.des_crypt",
- bsd_nthash = "passlib.handlers.windows",
- bsdi_crypt = "passlib.handlers.des_crypt",
- cisco_pix = "passlib.handlers.cisco",
- cisco_asa = "passlib.handlers.cisco",
- cisco_type7 = "passlib.handlers.cisco",
- cta_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
- crypt16 = "passlib.handlers.des_crypt",
- des_crypt = "passlib.handlers.des_crypt",
- django_argon2 = "passlib.handlers.django",
- django_bcrypt = "passlib.handlers.django",
- django_bcrypt_sha256 = "passlib.handlers.django",
- django_pbkdf2_sha256 = "passlib.handlers.django",
- django_pbkdf2_sha1 = "passlib.handlers.django",
- django_salted_sha1 = "passlib.handlers.django",
- django_salted_md5 = "passlib.handlers.django",
- django_des_crypt = "passlib.handlers.django",
- django_disabled = "passlib.handlers.django",
- dlitz_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
- fshp = "passlib.handlers.fshp",
- grub_pbkdf2_sha512 = "passlib.handlers.pbkdf2",
- hex_md4 = "passlib.handlers.digests",
- hex_md5 = "passlib.handlers.digests",
- hex_sha1 = "passlib.handlers.digests",
- hex_sha256 = "passlib.handlers.digests",
- hex_sha512 = "passlib.handlers.digests",
- htdigest = "passlib.handlers.digests",
- ldap_plaintext = "passlib.handlers.ldap_digests",
- ldap_md5 = "passlib.handlers.ldap_digests",
- ldap_sha1 = "passlib.handlers.ldap_digests",
- ldap_hex_md5 = "passlib.handlers.roundup",
- ldap_hex_sha1 = "passlib.handlers.roundup",
- ldap_salted_md5 = "passlib.handlers.ldap_digests",
- ldap_salted_sha1 = "passlib.handlers.ldap_digests",
- ldap_salted_sha256 = "passlib.handlers.ldap_digests",
- ldap_salted_sha512 = "passlib.handlers.ldap_digests",
- ldap_des_crypt = "passlib.handlers.ldap_digests",
- ldap_bsdi_crypt = "passlib.handlers.ldap_digests",
- ldap_md5_crypt = "passlib.handlers.ldap_digests",
- ldap_bcrypt = "passlib.handlers.ldap_digests",
- ldap_sha1_crypt = "passlib.handlers.ldap_digests",
- ldap_sha256_crypt = "passlib.handlers.ldap_digests",
- ldap_sha512_crypt = "passlib.handlers.ldap_digests",
- ldap_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
- ldap_pbkdf2_sha256 = "passlib.handlers.pbkdf2",
- ldap_pbkdf2_sha512 = "passlib.handlers.pbkdf2",
- lmhash = "passlib.handlers.windows",
- md5_crypt = "passlib.handlers.md5_crypt",
- msdcc = "passlib.handlers.windows",
- msdcc2 = "passlib.handlers.windows",
- mssql2000 = "passlib.handlers.mssql",
- mssql2005 = "passlib.handlers.mssql",
- mysql323 = "passlib.handlers.mysql",
- mysql41 = "passlib.handlers.mysql",
- nthash = "passlib.handlers.windows",
- oracle10 = "passlib.handlers.oracle",
- oracle11 = "passlib.handlers.oracle",
- pbkdf2_sha1 = "passlib.handlers.pbkdf2",
- pbkdf2_sha256 = "passlib.handlers.pbkdf2",
- pbkdf2_sha512 = "passlib.handlers.pbkdf2",
- phpass = "passlib.handlers.phpass",
- plaintext = "passlib.handlers.misc",
- postgres_md5 = "passlib.handlers.postgres",
- roundup_plaintext = "passlib.handlers.roundup",
- scram = "passlib.handlers.scram",
- scrypt = "passlib.handlers.scrypt",
- sha1_crypt = "passlib.handlers.sha1_crypt",
- sha256_crypt = "passlib.handlers.sha2_crypt",
- sha512_crypt = "passlib.handlers.sha2_crypt",
- sun_md5_crypt = "passlib.handlers.sun_md5_crypt",
- unix_disabled = "passlib.handlers.misc",
- unix_fallback = "passlib.handlers.misc",
- )
- # master regexp for detecting valid handler names
- _name_re = re.compile("^[a-z][a-z0-9_]+[a-z0-9]$")
- # names which aren't allowed for various reasons
- # (mainly keyword conflicts in CryptContext)
- _forbidden_names = frozenset(["onload", "policy", "context", "all",
- "default", "none", "auto"])
- #=============================================================================
- # registry frontend functions
- #=============================================================================
- def _validate_handler_name(name):
- """helper to validate handler name
- :raises ValueError:
- * if empty name
- * if name not lower case
- * if name contains double underscores
- * if name is reserved (e.g. ``context``, ``all``).
- """
- if not name:
- raise ValueError("handler name cannot be empty: %r" % (name,))
- if name.lower() != name:
- raise ValueError("name must be lower-case: %r" % (name,))
- if not _name_re.match(name):
- raise ValueError("invalid name (must be 3+ characters, "
- " begin with a-z, and contain only underscore, a-z, "
- "0-9): %r" % (name,))
- if '__' in name:
- raise ValueError("name may not contain double-underscores: %r" %
- (name,))
- if name in _forbidden_names:
- raise ValueError("that name is not allowed: %r" % (name,))
- return True
- def register_crypt_handler_path(name, path):
- """register location to lazy-load handler when requested.
- custom hashes may be registered via :func:`register_crypt_handler`,
- or they may be registered by this function,
- which will delay actually importing and loading the handler
- until a call to :func:`get_crypt_handler` is made for the specified name.
- :arg name: name of handler
- :arg path: module import path
- the specified module path should contain a password hash handler
- called :samp:`{name}`, or the path may contain a colon,
- specifying the module and module attribute to use.
- for example, the following would cause ``get_handler("myhash")`` to look
- for a class named ``myhash`` within the ``myapp.helpers`` module::
- >>> from passlib.registry import registry_crypt_handler_path
- >>> registry_crypt_handler_path("myhash", "myapp.helpers")
- ...while this form would cause ``get_handler("myhash")`` to look
- for a class name ``MyHash`` within the ``myapp.helpers`` module::
- >>> from passlib.registry import registry_crypt_handler_path
- >>> registry_crypt_handler_path("myhash", "myapp.helpers:MyHash")
- """
- # validate name
- _validate_handler_name(name)
- # validate path
- if path.startswith("."):
- raise ValueError("path cannot start with '.'")
- if ':' in path:
- if path.count(':') > 1:
- raise ValueError("path cannot have more than one ':'")
- if path.find('.', path.index(':')) > -1:
- raise ValueError("path cannot have '.' to right of ':'")
- # store location
- _locations[name] = path
- log.debug("registered path to %r handler: %r", name, path)
- def register_crypt_handler(handler, force=False, _attr=None):
- """register password hash handler.
- this method immediately registers a handler with the internal passlib registry,
- so that it will be returned by :func:`get_crypt_handler` when requested.
- :arg handler: the password hash handler to register
- :param force: force override of existing handler (defaults to False)
- :param _attr:
- [internal kwd] if specified, ensures ``handler.name``
- matches this value, or raises :exc:`ValueError`.
- :raises TypeError:
- if the specified object does not appear to be a valid handler.
- :raises ValueError:
- if the specified object's name (or other required attributes)
- contain invalid values.
- :raises KeyError:
- if a (different) handler was already registered with
- the same name, and ``force=True`` was not specified.
- """
- # validate handler
- if not is_crypt_handler(handler):
- raise ExpectedTypeError(handler, "password hash handler", "handler")
- if not handler:
- raise AssertionError("``bool(handler)`` must be True")
- # validate name
- name = handler.name
- _validate_handler_name(name)
- if _attr and _attr != name:
- raise ValueError("handlers must be stored only under their own name (%r != %r)" %
- (_attr, name))
- # check for existing handler
- other = _handlers.get(name)
- if other:
- if other is handler:
- log.debug("same %r handler already registered: %r", name, handler)
- return
- elif force:
- log.warning("overriding previously registered %r handler: %r",
- name, other)
- else:
- raise KeyError("another %r handler has already been registered: %r" %
- (name, other))
- # register handler
- _handlers[name] = handler
- log.debug("registered %r handler: %r", name, handler)
- def get_crypt_handler(name, default=_UNSET):
- """return handler for specified password hash scheme.
- this method looks up a handler for the specified scheme.
- if the handler is not already loaded,
- it checks if the location is known, and loads it first.
- :arg name: name of handler to return
- :param default: optional default value to return if no handler with specified name is found.
- :raises KeyError: if no handler matching that name is found, and no default specified, a KeyError will be raised.
- :returns: handler attached to name, or default value (if specified).
- """
- # catch invalid names before we check _handlers,
- # since it's a module dict, and exposes things like __package__, etc.
- if name.startswith("_"):
- if default is _UNSET:
- raise KeyError("invalid handler name: %r" % (name,))
- else:
- return default
- # check if handler is already loaded
- try:
- return _handlers[name]
- except KeyError:
- pass
- # normalize name (and if changed, check dict again)
- assert isinstance(name, unicode_or_str), "name must be string instance"
- alt = name.replace("-","_").lower()
- if alt != name:
- warn("handler names should be lower-case, and use underscores instead "
- "of hyphens: %r => %r" % (name, alt), PasslibWarning,
- stacklevel=2)
- name = alt
- # try to load using new name
- try:
- return _handlers[name]
- except KeyError:
- pass
- # check if lazy load mapping has been specified for this driver
- path = _locations.get(name)
- if path:
- if ':' in path:
- modname, modattr = path.split(":")
- else:
- modname, modattr = path, name
- ##log.debug("loading %r handler from path: '%s:%s'", name, modname, modattr)
- # try to load the module - any import errors indicate runtime config, usually
- # either missing package, or bad path provided to register_crypt_handler_path()
- mod = __import__(modname, fromlist=[modattr], level=0)
- # first check if importing module triggered register_crypt_handler(),
- # (this is discouraged due to its magical implicitness)
- handler = _handlers.get(name)
- if handler:
- # XXX: issue deprecation warning here?
- assert is_crypt_handler(handler), "unexpected object: name=%r object=%r" % (name, handler)
- return handler
- # then get real handler & register it
- handler = getattr(mod, modattr)
- register_crypt_handler(handler, _attr=name)
- return handler
- # fail!
- if default is _UNSET:
- raise KeyError("no crypt handler found for algorithm: %r" % (name,))
- else:
- return default
- def list_crypt_handlers(loaded_only=False):
- """return sorted list of all known crypt handler names.
- :param loaded_only: if ``True``, only returns names of handlers which have actually been loaded.
- :returns: list of names of all known handlers
- """
- names = set(_handlers)
- if not loaded_only:
- names.update(_locations)
- # strip private attrs out of namespace and sort.
- # TODO: make _handlers a separate list, so we don't have module namespace mixed in.
- return sorted(name for name in names if not name.startswith("_"))
- # NOTE: these two functions mainly exist just for the unittests...
- def _has_crypt_handler(name, loaded_only=False):
- """check if handler name is known.
- this is only useful for two cases:
- * quickly checking if handler has already been loaded
- * checking if handler exists, without actually loading it
- :arg name: name of handler
- :param loaded_only: if ``True``, returns False if handler exists but hasn't been loaded
- """
- return (name in _handlers) or (not loaded_only and name in _locations)
- def _unload_handler_name(name, locations=True):
- """unloads a handler from the registry.
- .. warning::
- this is an internal function,
- used only by the unittests.
- if loaded handler is found with specified name, it's removed.
- if path to lazy load handler is found, it's removed.
- missing names are a noop.
- :arg name: name of handler to unload
- :param locations: if False, won't purge registered handler locations (default True)
- """
- if name in _handlers:
- del _handlers[name]
- if locations and name in _locations:
- del _locations[name]
- #=============================================================================
- # inspection helpers
- #=============================================================================
- #------------------------------------------------------------------
- # general
- #------------------------------------------------------------------
- # TODO: needs UTs
- def _resolve(hasher, param="value"):
- """
- internal helper to resolve argument to hasher object
- """
- if is_crypt_handler(hasher):
- return hasher
- elif isinstance(hasher, unicode_or_str):
- return get_crypt_handler(hasher)
- else:
- raise exc.ExpectedTypeError(hasher, unicode_or_str, param)
- #: backend aliases
- ANY = "any"
- BUILTIN = "builtin"
- OS_CRYPT = "os_crypt"
- # TODO: needs UTs
- def has_backend(hasher, backend=ANY, safe=False):
- """
- Test if specified backend is available for hasher.
- :param hasher:
- Hasher name or object.
- :param backend:
- Name of backend, or ``"any"`` if any backend will do.
- For hashers without multiple backends, will pretend
- they have a single backend named ``"builtin"``.
- :param safe:
- By default, throws error if backend is unknown.
- If ``safe=True``, will just return false value.
- :raises ValueError:
- * if hasher name is unknown.
- * if backend is unknown to hasher, and safe=False.
- :return:
- True if backend available, False if not available,
- and None if unknown + safe=True.
- """
- hasher = _resolve(hasher)
- if backend == ANY:
- if not hasattr(hasher, "get_backend"):
- # single backend, assume it's loaded
- return True
- # multiple backends, check at least one is loadable
- try:
- hasher.get_backend()
- return True
- except exc.MissingBackendError:
- return False
- # test for specific backend
- if hasattr(hasher, "has_backend"):
- # multiple backends
- if safe and backend not in hasher.backends:
- return None
- return hasher.has_backend(backend)
- # single builtin backend
- if backend == BUILTIN:
- return True
- elif safe:
- return None
- else:
- raise exc.UnknownBackendError(hasher, backend)
- #------------------------------------------------------------------
- # os crypt
- #------------------------------------------------------------------
- # TODO: move unix_crypt_schemes list to here.
- # os_crypt_schemes -- alias for unix_crypt_schemes above
- # TODO: needs UTs
- @memoize_single_value
- def get_supported_os_crypt_schemes():
- """
- return tuple of schemes which :func:`crypt.crypt` natively supports.
- """
- if not os_crypt_present:
- return ()
- cache = tuple(name for name in os_crypt_schemes
- if get_crypt_handler(name).has_backend(OS_CRYPT))
- if not cache: # pragma: no cover -- sanity check
- # no idea what OS this could happen on...
- import platform
- warn("crypt.crypt() function is present, but doesn't support any "
- "formats known to passlib! (system=%r release=%r)" %
- (platform.system(), platform.release()),
- exc.PasslibRuntimeWarning)
- return cache
- # TODO: needs UTs
- def has_os_crypt_support(hasher):
- """
- check if hash is supported by native :func:`crypt.crypt` function.
- if :func:`crypt.crypt` is not present, will always return False.
- :param hasher:
- name or hasher object.
- :returns bool:
- True if hash format is supported by OS, else False.
- """
- return os_crypt_present and has_backend(hasher, OS_CRYPT, safe=True)
- #=============================================================================
- # eof
- #=============================================================================
|