548 lines
20 KiB
Python
548 lines
20 KiB
Python
"""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
|
|
#=============================================================================
|