From fdbd09ed5135b4c9a86e30270d2fd7ff9b22ca40 Mon Sep 17 00:00:00 2001 From: Polina Date: Thu, 2 Jul 2026 18:53:58 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?venv/Lib/site-packages/passlib=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- venv/Lib/site-packages/passlib/hash.py | 68 ++ venv/Lib/site-packages/passlib/hosts.py | 106 +++ venv/Lib/site-packages/passlib/ifc.py | 353 +++++++++ venv/Lib/site-packages/passlib/pwd.py | 809 +++++++++++++++++++++ venv/Lib/site-packages/passlib/registry.py | 547 ++++++++++++++ 5 files changed, 1883 insertions(+) create mode 100644 venv/Lib/site-packages/passlib/hash.py create mode 100644 venv/Lib/site-packages/passlib/hosts.py create mode 100644 venv/Lib/site-packages/passlib/ifc.py create mode 100644 venv/Lib/site-packages/passlib/pwd.py create mode 100644 venv/Lib/site-packages/passlib/registry.py diff --git a/venv/Lib/site-packages/passlib/hash.py b/venv/Lib/site-packages/passlib/hash.py new file mode 100644 index 0000000..2cc0628 --- /dev/null +++ b/venv/Lib/site-packages/passlib/hash.py @@ -0,0 +1,68 @@ +""" +passlib.hash - proxy object mapping hash scheme names -> handlers + +================== +***** NOTICE ***** +================== + +This module does not actually contain any hashes. This file +is a stub that replaces itself with a proxy object. + +This proxy object (passlib.registry._PasslibRegistryProxy) +handles lazy-loading hashes as they are requested. + +The actual implementation of the various hashes is store elsewhere, +mainly in the submodules of the ``passlib.handlers`` subpackage. +""" + +#============================================================================= +# import proxy object and replace this module +#============================================================================= + +# XXX: if any platform has problem w/ lazy modules, could support 'non-lazy' +# version which just imports all schemes known to list_crypt_handlers() + +from passlib.registry import _proxy +import sys +sys.modules[__name__] = _proxy + +#============================================================================= +# HACK: the following bit of code is unreachable, but it's presence seems to +# help make autocomplete work for certain IDEs such as PyCharm. +# this list is automatically regenerated using $SOURCE/admin/regen.py +#============================================================================= + +#---------------------------------------------------- +# begin autocomplete hack (autogenerated 2016-11-10) +#---------------------------------------------------- +if False: + from passlib.handlers.argon2 import argon2 + from passlib.handlers.bcrypt import bcrypt, bcrypt_sha256 + from passlib.handlers.cisco import cisco_asa, cisco_pix, cisco_type7 + from passlib.handlers.des_crypt import bigcrypt, bsdi_crypt, crypt16, des_crypt + from passlib.handlers.digests import hex_md4, hex_md5, hex_sha1, hex_sha256, hex_sha512, htdigest + from passlib.handlers.django import django_bcrypt, django_bcrypt_sha256, django_des_crypt, django_disabled, django_pbkdf2_sha1, django_pbkdf2_sha256, django_salted_md5, django_salted_sha1 + from passlib.handlers.fshp import fshp + from passlib.handlers.ldap_digests import ldap_bcrypt, ldap_bsdi_crypt, ldap_des_crypt, ldap_md5, ldap_md5_crypt, ldap_plaintext, ldap_salted_md5, ldap_salted_sha1, ldap_salted_sha256, ldap_salted_sha512, ldap_sha1, ldap_sha1_crypt, ldap_sha256_crypt, ldap_sha512_crypt + from passlib.handlers.md5_crypt import apr_md5_crypt, md5_crypt + from passlib.handlers.misc import plaintext, unix_disabled, unix_fallback + from passlib.handlers.mssql import mssql2000, mssql2005 + from passlib.handlers.mysql import mysql323, mysql41 + from passlib.handlers.oracle import oracle10, oracle11 + from passlib.handlers.pbkdf2 import atlassian_pbkdf2_sha1, cta_pbkdf2_sha1, dlitz_pbkdf2_sha1, grub_pbkdf2_sha512, ldap_pbkdf2_sha1, ldap_pbkdf2_sha256, ldap_pbkdf2_sha512, pbkdf2_sha1, pbkdf2_sha256, pbkdf2_sha512 + from passlib.handlers.phpass import phpass + from passlib.handlers.postgres import postgres_md5 + from passlib.handlers.roundup import ldap_hex_md5, ldap_hex_sha1, roundup_plaintext + from passlib.handlers.scram import scram + from passlib.handlers.scrypt import scrypt + from passlib.handlers.sha1_crypt import sha1_crypt + from passlib.handlers.sha2_crypt import sha256_crypt, sha512_crypt + from passlib.handlers.sun_md5_crypt import sun_md5_crypt + from passlib.handlers.windows import bsd_nthash, lmhash, msdcc, msdcc2, nthash +#---------------------------------------------------- +# end autocomplete hack +#---------------------------------------------------- + +#============================================================================= +# eoc +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/hosts.py b/venv/Lib/site-packages/passlib/hosts.py new file mode 100644 index 0000000..1f137a2 --- /dev/null +++ b/venv/Lib/site-packages/passlib/hosts.py @@ -0,0 +1,106 @@ +"""passlib.hosts""" +#============================================================================= +# imports +#============================================================================= +# core +from warnings import warn +# pkg +from passlib.context import LazyCryptContext +from passlib.exc import PasslibRuntimeWarning +from passlib import registry +from passlib.utils import has_crypt, unix_crypt_schemes +# local +__all__ = [ + "linux_context", "linux2_context", + "openbsd_context", + "netbsd_context", + "freebsd_context", + "host_context", +] + +#============================================================================= +# linux support +#============================================================================= + +# known platform names - linux2 + +linux_context = linux2_context = LazyCryptContext( + schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt", + "des_crypt", "unix_disabled" ], + deprecated = [ "des_crypt" ], + ) + +#============================================================================= +# bsd support +#============================================================================= + +# known platform names - +# freebsd2 +# freebsd3 +# freebsd4 +# freebsd5 +# freebsd6 +# freebsd7 +# +# netbsd1 + +# referencing source via -http://fxr.googlebit.com +# freebsd 6,7,8 - des, md5, bcrypt, bsd_nthash +# netbsd - des, ext, md5, bcrypt, sha1 +# openbsd - des, ext, md5, bcrypt + +freebsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsd_nthash", + "des_crypt", "unix_disabled"]) + +openbsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsdi_crypt", + "des_crypt", "unix_disabled"]) + +netbsd_context = LazyCryptContext(["bcrypt", "sha1_crypt", "md5_crypt", + "bsdi_crypt", "des_crypt", "unix_disabled"]) + +# XXX: include darwin in this list? it's got a BSD crypt variant, +# but that's not what it uses for user passwords. + +#============================================================================= +# current host +#============================================================================= +if registry.os_crypt_present: + # NOTE: this is basically mimicing the output of os crypt(), + # except that it uses passlib's (usually stronger) defaults settings, + # and can be inspected and used much more flexibly. + + def _iter_os_crypt_schemes(): + """helper which iterates over supported os_crypt schemes""" + out = registry.get_supported_os_crypt_schemes() + if out: + # only offer disabled handler if there's another scheme in front, + # as this can't actually hash any passwords + out += ("unix_disabled",) + return out + + host_context = LazyCryptContext(_iter_os_crypt_schemes()) + +#============================================================================= +# other platforms +#============================================================================= + +# known platform strings - +# aix3 +# aix4 +# atheos +# beos5 +# darwin +# generic +# hp-ux11 +# irix5 +# irix6 +# mac +# next3 +# os2emx +# riscos +# sunos5 +# unixware7 + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/ifc.py b/venv/Lib/site-packages/passlib/ifc.py new file mode 100644 index 0000000..559d256 --- /dev/null +++ b/venv/Lib/site-packages/passlib/ifc.py @@ -0,0 +1,353 @@ +"""passlib.ifc - abstract interfaces used by Passlib""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +import sys +# site +# pkg +from passlib.utils.decor import deprecated_method +# local +__all__ = [ + "PasswordHash", +] + +#============================================================================= +# 2/3 compatibility helpers +#============================================================================= +def recreate_with_metaclass(meta): + """class decorator that re-creates class using metaclass""" + def builder(cls): + if meta is type(cls): + return cls + return meta(cls.__name__, cls.__bases__, cls.__dict__.copy()) + return builder + +#============================================================================= +# PasswordHash interface +#============================================================================= +from abc import ABCMeta, abstractmethod, abstractproperty + +# TODO: make this actually use abstractproperty(), +# now that we dropped py25, 'abc' is always available. + +# XXX: rename to PasswordHasher? + +@recreate_with_metaclass(ABCMeta) +class PasswordHash(object): + """This class describes an abstract interface which all password hashes + in Passlib adhere to. Under Python 2.6 and up, this is an actual + Abstract Base Class built using the :mod:`!abc` module. + + See the Passlib docs for full documentation. + """ + #=================================================================== + # class attributes + #=================================================================== + + #--------------------------------------------------------------- + # general information + #--------------------------------------------------------------- + ##name + ##setting_kwds + ##context_kwds + + #: flag which indicates this hasher matches a "disabled" hash + #: (e.g. unix_disabled, or django_disabled); and doesn't actually + #: depend on the provided password. + is_disabled = False + + #: Should be None, or a positive integer indicating hash + #: doesn't support secrets larger than this value. + #: Whether hash throws error or silently truncates secret + #: depends on .truncate_error and .truncate_verify_reject flags below. + #: NOTE: calls may treat as boolean, since value will never be 0. + #: .. versionadded:: 1.7 + #: .. TODO: passlib 1.8: deprecate/rename this attr to "max_secret_size"? + truncate_size = None + + # NOTE: these next two default to the optimistic "ideal", + # most hashes in passlib have to default to False + # for backward compat and/or expected behavior with existing hashes. + + #: If True, .hash() should throw a :exc:`~passlib.exc.PasswordSizeError` for + #: any secrets larger than .truncate_size. Many hashers default to False + #: for historical / compatibility purposes, indicating they will silently + #: truncate instead. All such hashers SHOULD support changing + #: the policy via ``.using(truncate_error=True)``. + #: .. versionadded:: 1.7 + #: .. TODO: passlib 1.8: deprecate/rename this attr to "truncate_hash_error"? + truncate_error = True + + #: If True, .verify() should reject secrets larger than max_password_size. + #: Many hashers default to False for historical / compatibility purposes, + #: indicating they will match on the truncated portion instead. + #: .. versionadded:: 1.7.1 + truncate_verify_reject = True + + #--------------------------------------------------------------- + # salt information -- if 'salt' in setting_kwds + #--------------------------------------------------------------- + ##min_salt_size + ##max_salt_size + ##default_salt_size + ##salt_chars + ##default_salt_chars + + #--------------------------------------------------------------- + # rounds information -- if 'rounds' in setting_kwds + #--------------------------------------------------------------- + ##min_rounds + ##max_rounds + ##default_rounds + ##rounds_cost + + #--------------------------------------------------------------- + # encoding info -- if 'encoding' in context_kwds + #--------------------------------------------------------------- + ##default_encoding + + #=================================================================== + # primary methods + #=================================================================== + @classmethod + @abstractmethod + def hash(cls, secret, # * + **setting_and_context_kwds): # pragma: no cover -- abstract method + r""" + Hash secret, returning result. + Should handle generating salt, etc, and should return string + containing identifier, salt & other configuration, as well as digest. + + :param \\*\\*settings_kwds: + + Pass in settings to customize configuration of resulting hash. + + .. deprecated:: 1.7 + + Starting with Passlib 1.7, callers should no longer pass settings keywords + (e.g. ``rounds`` or ``salt`` directly to :meth:`!hash`); should use + ``.using(**settings).hash(secret)`` construction instead. + + Support will be removed in Passlib 2.0. + + :param \\*\\*context_kwds: + + Specific algorithms may require context-specific information (such as the user login). + """ + # FIXME: need stub for classes that define .encrypt() instead ... + # this should call .encrypt(), and check for recursion back to here. + raise NotImplementedError("must be implemented by subclass") + + @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()") + @classmethod + def encrypt(cls, *args, **kwds): + """ + Legacy alias for :meth:`hash`. + + .. deprecated:: 1.7 + This method was renamed to :meth:`!hash` in version 1.7. + This alias will be removed in version 2.0, and should only + be used for compatibility with Passlib 1.3 - 1.6. + """ + return cls.hash(*args, **kwds) + + # XXX: could provide default implementation which hands value to + # hash(), and then does constant-time comparision on the result + # (after making both are same string type) + @classmethod + @abstractmethod + def verify(cls, secret, hash, **context_kwds): # pragma: no cover -- abstract method + """verify secret against hash, returns True/False""" + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # configuration + #=================================================================== + @classmethod + @abstractmethod + def using(cls, relaxed=False, **kwds): + """ + Return another hasher object (typically a subclass of the current one), + which integrates the configuration options specified by ``kwds``. + This should *always* return a new object, even if no configuration options are changed. + + .. todo:: + + document which options are accepted. + + :returns: + typically returns a subclass for most hasher implementations. + + .. todo:: + + add this method to main documentation. + """ + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # migration + #=================================================================== + @classmethod + def needs_update(cls, hash, secret=None): + """ + check if hash's configuration is outside desired bounds, + or contains some other internal option which requires + updating the password hash. + + :param hash: + hash string to examine + + :param secret: + optional secret known to have verified against the provided hash. + (this is used by some hashes to detect legacy algorithm mistakes). + + :return: + whether secret needs re-hashing. + + .. versionadded:: 1.7 + """ + # by default, always report that we don't need update + return False + + #=================================================================== + # additional methods + #=================================================================== + @classmethod + @abstractmethod + def identify(cls, hash): # pragma: no cover -- abstract method + """check if hash belongs to this scheme, returns True/False""" + raise NotImplementedError("must be implemented by subclass") + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method + """ + compile settings into a configuration string for genhash() + + .. deprecated:: 1.7 + + As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. + + For all known real-world uses, hashing a constant string + should provide equivalent functionality. + + This deprecation may be reversed if a use-case presents itself in the mean time. + """ + # NOTE: this fallback runs full hash alg, w/ whatever cost param is passed along. + # implementations (esp ones w/ variable cost) will want to subclass this + # with a constant-time implementation that just renders a config string. + if cls.context_kwds: + raise NotImplementedError("must be implemented by subclass") + return cls.using(**setting_kwds).hash("") + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, **context): + """ + generated hash for secret, using settings from config/hash string + + .. deprecated:: 1.7 + + As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. + + This deprecation may be reversed if a use-case presents itself in the mean time. + """ + # XXX: if hashes reliably offered a .parse() method, could make a fallback for this. + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # undocumented methods / attributes + #=================================================================== + # the following entry points are used internally by passlib, + # and aren't documented as part of the exposed interface. + # they are subject to change between releases, + # but are documented here so there's a list of them *somewhere*. + + #--------------------------------------------------------------- + # extra metdata + #--------------------------------------------------------------- + + #: this attribute shouldn't be used by hashers themselves, + #: it's reserved for the CryptContext to track which hashers are deprecated. + #: Note the context will only set this on objects it owns (and generated by .using()), + #: and WONT set it on global objects. + #: [added in 1.7] + #: TODO: document this, or at least the use of testing for + #: 'CryptContext().handler().deprecated' + deprecated = False + + #: optionally present if hasher corresponds to format built into Django. + #: this attribute (if not None) should be the Django 'algorithm' name. + #: also indicates to passlib.ext.django that (when installed in django), + #: django's native hasher should be used in preference to this one. + ## django_name + + #--------------------------------------------------------------- + # checksum information - defined for many hashes + #--------------------------------------------------------------- + ## checksum_chars + ## checksum_size + + #--------------------------------------------------------------- + # experimental methods + #--------------------------------------------------------------- + + ##@classmethod + ##def normhash(cls, hash): + ## """helper to clean up non-canonic instances of hash. + ## currently only provided by bcrypt() to fix an historical passlib issue. + ## """ + + # experimental helper to parse hash into components. + ##@classmethod + ##def parsehash(cls, hash, checksum=True, sanitize=False): + ## """helper to parse hash into components, returns dict""" + + # experiment helper to estimate bitsize of different hashes, + # implement for GenericHandler, but may be currently be off for some hashes. + # want to expand this into a way to programmatically compare + # "strengths" of different hashes and hash algorithms. + # still needs to have some factor for estimate relative cost per round, + # ala in the style of the scrypt whitepaper. + ##@classmethod + ##def bitsize(cls, **kwds): + ## """returns dict mapping component -> bits contributed. + ## components currently include checksum, salt, rounds. + ## """ + + #=================================================================== + # eoc + #=================================================================== + +class DisabledHash(PasswordHash): + """ + extended disabled-hash methods; only need be present if .disabled = True + """ + + is_disabled = True + + @classmethod + def disable(cls, hash=None): + """ + return string representing a 'disabled' hash; + optionally including previously enabled hash + (this is up to the individual scheme). + """ + # default behavior: ignore original hash, return standalone marker + return cls.hash("") + + @classmethod + def enable(cls, hash): + """ + given a disabled-hash string, + extract previously-enabled hash if one is present, + otherwise raises ValueError + """ + # default behavior: no way to restore original hash + raise ValueError("cannot restore original hash") + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/pwd.py b/venv/Lib/site-packages/passlib/pwd.py new file mode 100644 index 0000000..27ed228 --- /dev/null +++ b/venv/Lib/site-packages/passlib/pwd.py @@ -0,0 +1,809 @@ +"""passlib.pwd -- password generation helpers""" +#============================================================================= +# imports +#============================================================================= +from __future__ import absolute_import, division, print_function, unicode_literals +# core +import codecs +from collections import defaultdict +try: + from collections.abc import MutableMapping +except ImportError: + # py2 compat + from collections import MutableMapping +from math import ceil, log as logf +import logging; log = logging.getLogger(__name__) +import pkg_resources +import os +# site +# pkg +from passlib import exc +from passlib.utils.compat import PY2, irange, itervalues, int_types +from passlib.utils import rng, getrandstr, to_unicode +from passlib.utils.decor import memoized_property +# local +__all__ = [ + "genword", "default_charsets", + "genphrase", "default_wordsets", +] + +#============================================================================= +# constants +#============================================================================= + +# XXX: rename / publically document this map? +entropy_aliases = dict( + # barest protection from throttled online attack + unsafe=12, + + # some protection from unthrottled online attack + weak=24, + + # some protection from offline attacks + fair=36, + + # reasonable protection from offline attacks + strong=48, + + # very good protection from offline attacks + secure=60, +) + +#============================================================================= +# internal helpers +#============================================================================= + +def _superclasses(obj, cls): + """return remaining classes in object's MRO after cls""" + mro = type(obj).__mro__ + return mro[mro.index(cls)+1:] + + +def _self_info_rate(source): + """ + returns 'rate of self-information' -- + i.e. average (per-symbol) entropy of the sequence **source**, + where probability of a given symbol occurring is calculated based on + the number of occurrences within the sequence itself. + + if all elements of the source are unique, this should equal ``log(len(source), 2)``. + + :arg source: + iterable containing 0+ symbols + (e.g. list of strings or ints, string of characters, etc). + + :returns: + float bits of entropy + """ + try: + size = len(source) + except TypeError: + # if len() doesn't work, calculate size by summing counts later + size = None + counts = defaultdict(int) + for char in source: + counts[char] += 1 + if size is None: + values = counts.values() + size = sum(values) + else: + values = itervalues(counts) + if not size: + return 0 + # NOTE: the following performs ``- sum(value / size * logf(value / size, 2) for value in values)``, + # it just does so with as much pulled out of the sum() loop as possible... + return logf(size, 2) - sum(value * logf(value, 2) for value in values) / size + + +# def _total_self_info(source): +# """ +# return total self-entropy of a sequence +# (the average entropy per symbol * size of sequence) +# """ +# return _self_info_rate(source) * len(source) + + +def _open_asset_path(path, encoding=None): + """ + :param asset_path: + string containing absolute path to file, + or package-relative path using format + ``"python.module:relative/file/path"``. + + :returns: + filehandle opened in 'rb' mode + (unless encoding explicitly specified) + """ + if encoding: + return codecs.getreader(encoding)(_open_asset_path(path)) + if os.path.isabs(path): + return open(path, "rb") + package, sep, subpath = path.partition(":") + if not sep: + raise ValueError("asset path must be absolute file path " + "or use 'pkg.name:sub/path' format: %r" % (path,)) + return pkg_resources.resource_stream(package, subpath) + + +#: type aliases +_sequence_types = (list, tuple) +_set_types = (set, frozenset) + +#: set of elements that ensure_unique() has validated already. +_ensure_unique_cache = set() + + +def _ensure_unique(source, param="source"): + """ + helper for generators -- + Throws ValueError if source elements aren't unique. + Error message will display (abbreviated) repr of the duplicates in a string/list + """ + # check cache to speed things up for frozensets / tuples / strings + cache = _ensure_unique_cache + hashable = True + try: + if source in cache: + return True + except TypeError: + hashable = False + + # check if it has dup elements + if isinstance(source, _set_types) or len(set(source)) == len(source): + if hashable: + try: + cache.add(source) + except TypeError: + # XXX: under pypy, "list() in set()" above doesn't throw TypeError, + # but trying to add unhashable it to a set *does*. + pass + return True + + # build list of duplicate values + seen = set() + dups = set() + for elem in source: + (dups if elem in seen else seen).add(elem) + dups = sorted(dups) + trunc = 8 + if len(dups) > trunc: + trunc = 5 + dup_repr = ", ".join(repr(str(word)) for word in dups[:trunc]) + if len(dups) > trunc: + dup_repr += ", ... plus %d others" % (len(dups) - trunc) + + # throw error + raise ValueError("`%s` cannot contain duplicate elements: %s" % + (param, dup_repr)) + +#============================================================================= +# base generator class +#============================================================================= +class SequenceGenerator(object): + """ + Base class used by word & phrase generators. + + These objects take a series of options, corresponding + to those of the :func:`generate` function. + They act as callables which can be used to generate a password + or a list of 1+ passwords. They also expose some read-only + informational attributes. + + Parameters + ---------- + :param entropy: + Optionally specify the amount of entropy the resulting passwords + should contain (as measured with respect to the generator itself). + This will be used to auto-calculate the required password size. + + :param length: + Optionally specify the length of password to generate, + measured as count of whatever symbols the subclass uses (characters or words). + Note if ``entropy`` requires a larger minimum length, + that will be used instead. + + :param rng: + Optionally provide a custom RNG source to use. + Should be an instance of :class:`random.Random`, + defaults to :class:`random.SystemRandom`. + + Attributes + ---------- + .. autoattribute:: length + .. autoattribute:: symbol_count + .. autoattribute:: entropy_per_symbol + .. autoattribute:: entropy + + Subclassing + ----------- + Subclasses must implement the ``.__next__()`` method, + and set ``.symbol_count`` before calling base ``__init__`` method. + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: requested size of final password + length = None + + #: requested entropy of final password + requested_entropy = "strong" + + #: random number source to use + rng = rng + + #: number of potential symbols (must be filled in by subclass) + symbol_count = None + + #============================================================================= + # init + #============================================================================= + def __init__(self, entropy=None, length=None, rng=None, **kwds): + + # make sure subclass set things up correctly + assert self.symbol_count is not None, "subclass must set .symbol_count" + + # init length & requested entropy + if entropy is not None or length is None: + if entropy is None: + entropy = self.requested_entropy + entropy = entropy_aliases.get(entropy, entropy) + if entropy <= 0: + raise ValueError("`entropy` must be positive number") + min_length = int(ceil(entropy / self.entropy_per_symbol)) + if length is None or length < min_length: + length = min_length + + self.requested_entropy = entropy + + if length < 1: + raise ValueError("`length` must be positive integer") + self.length = length + + # init other common options + if rng is not None: + self.rng = rng + + # hand off to parent + if kwds and _superclasses(self, SequenceGenerator) == (object,): + raise TypeError("Unexpected keyword(s): %s" % ", ".join(kwds.keys())) + super(SequenceGenerator, self).__init__(**kwds) + + #============================================================================= + # informational helpers + #============================================================================= + + @memoized_property + def entropy_per_symbol(self): + """ + Average entropy per symbol (assuming all symbols have equal probability) + """ + return logf(self.symbol_count, 2) + + @memoized_property + def entropy(self): + """ + Effective entropy of generated passwords. + + This value will always be a multiple of :attr:`entropy_per_symbol`. + If entropy is specified in constructor, :attr:`length` will be chosen so + so that this value is the smallest multiple >= :attr:`requested_entropy`. + """ + return self.length * self.entropy_per_symbol + + #============================================================================= + # generation + #============================================================================= + def __next__(self): + """main generation function, should create one password/phrase""" + raise NotImplementedError("implement in subclass") + + def __call__(self, returns=None): + """ + frontend used by genword() / genphrase() to create passwords + """ + if returns is None: + return next(self) + elif isinstance(returns, int_types): + return [next(self) for _ in irange(returns)] + elif returns is iter: + return self + else: + raise exc.ExpectedTypeError(returns, ", int, or ", "returns") + + def __iter__(self): + return self + + if PY2: + def next(self): + return self.__next__() + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# default charsets +#============================================================================= + +#: global dict of predefined characters sets +default_charsets = dict( + # ascii letters, digits, and some punctuation + ascii_72='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*?/', + + # ascii letters and digits + ascii_62='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + + # ascii_50, without visually similar '1IiLl', '0Oo', '5S', '8B' + ascii_50='234679abcdefghjkmnpqrstuvwxyzACDEFGHJKMNPQRTUVWXYZ', + + # lower case hexadecimal + hex='0123456789abcdef', +) + +#============================================================================= +# password generator +#============================================================================= + +class WordGenerator(SequenceGenerator): + """ + Class which generates passwords by randomly choosing from a string of unique characters. + + Parameters + ---------- + :param chars: + custom character string to draw from. + + :param charset: + predefined charset to draw from. + + :param \\*\\*kwds: + all other keywords passed to the :class:`SequenceGenerator` parent class. + + Attributes + ---------- + .. autoattribute:: chars + .. autoattribute:: charset + .. autoattribute:: default_charsets + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: Predefined character set in use (set to None for instances using custom 'chars') + charset = "ascii_62" + + #: string of chars to draw from -- usually filled in from charset + chars = None + + #============================================================================= + # init + #============================================================================= + def __init__(self, chars=None, charset=None, **kwds): + + # init chars and charset + if chars: + if charset: + raise TypeError("`chars` and `charset` are mutually exclusive") + else: + if not charset: + charset = self.charset + assert charset + chars = default_charsets[charset] + self.charset = charset + chars = to_unicode(chars, param="chars") + _ensure_unique(chars, param="chars") + self.chars = chars + + # hand off to parent + super(WordGenerator, self).__init__(**kwds) + # log.debug("WordGenerator(): entropy/char=%r", self.entropy_per_symbol) + + #============================================================================= + # informational helpers + #============================================================================= + + @memoized_property + def symbol_count(self): + return len(self.chars) + + #============================================================================= + # generation + #============================================================================= + + def __next__(self): + # XXX: could do things like optionally ensure certain character groups + # (e.g. letters & punctuation) are included + return getrandstr(self.rng, self.chars, self.length) + + #============================================================================= + # eoc + #============================================================================= + + +def genword(entropy=None, length=None, returns=None, **kwds): + """Generate one or more random passwords. + + This function uses :mod:`random.SystemRandom` to generate + one or more passwords using various character sets. + The complexity of the password can be specified + by size, or by the desired amount of entropy. + + Usage Example:: + + >>> # generate a random alphanumeric string with 48 bits of entropy (the default) + >>> from passlib import pwd + >>> pwd.genword() + 'DnBHvDjMK6' + + >>> # generate a random hexadecimal string with 52 bits of entropy + >>> pwd.genword(entropy=52, charset="hex") + '310f1a7ac793f' + + :param entropy: + Strength of resulting password, measured in 'guessing entropy' bits. + An appropriate **length** value will be calculated + based on the requested entropy amount, and the size of the character set. + + This can be a positive integer, or one of the following preset + strings: ``"weak"`` (24), ``"fair"`` (36), + ``"strong"`` (48), and ``"secure"`` (56). + + If neither this or **length** is specified, **entropy** will default + to ``"strong"`` (48). + + :param length: + Size of resulting password, measured in characters. + If omitted, the size is auto-calculated based on the **entropy** parameter. + + If both **entropy** and **length** are specified, + the stronger value will be used. + + :param returns: + Controls what this function returns: + + * If ``None`` (the default), this function will generate a single password. + * If an integer, this function will return a list containing that many passwords. + * If the ``iter`` constant, will return an iterator that yields passwords. + + :param chars: + + Optionally specify custom string of characters to use when randomly + generating a password. This option cannot be combined with **charset**. + + :param charset: + + The predefined character set to draw from (if not specified by **chars**). + There are currently four presets available: + + * ``"ascii_62"`` (the default) -- all digits and ascii upper & lowercase letters. + Provides ~5.95 entropy per character. + + * ``"ascii_50"`` -- subset which excludes visually similar characters + (``1IiLl0Oo5S8B``). Provides ~5.64 entropy per character. + + * ``"ascii_72"`` -- all digits and ascii upper & lowercase letters, + as well as some punctuation. Provides ~6.17 entropy per character. + + * ``"hex"`` -- Lower case hexadecimal. Providers 4 bits of entropy per character. + + :returns: + :class:`!unicode` string containing randomly generated password; + or list of 1+ passwords if :samp:`returns={int}` is specified. + """ + gen = WordGenerator(length=length, entropy=entropy, **kwds) + return gen(returns) + +#============================================================================= +# default wordsets +#============================================================================= + +def _load_wordset(asset_path): + """ + load wordset from compressed datafile within package data. + file should be utf-8 encoded + + :param asset_path: + string containing absolute path to wordset file, + or "python.module:relative/file/path". + + :returns: + tuple of words, as loaded from specified words file. + """ + # open resource file, convert to tuple of words (strip blank lines & ws) + with _open_asset_path(asset_path, "utf-8") as fh: + gen = (word.strip() for word in fh) + words = tuple(word for word in gen if word) + + # NOTE: works but not used + # # detect if file uses " " format, and strip numeric prefix + # def extract(row): + # idx, word = row.replace("\t", " ").split(" ", 1) + # if not idx.isdigit(): + # raise ValueError("row is not dice index + word") + # return word + # try: + # extract(words[-1]) + # except ValueError: + # pass + # else: + # words = tuple(extract(word) for word in words) + + log.debug("loaded %d-element wordset from %r", len(words), asset_path) + return words + + +class WordsetDict(MutableMapping): + """ + Special mapping used to store dictionary of wordsets. + Different from a regular dict in that some wordsets + may be lazy-loaded from an asset path. + """ + + #: dict of key -> asset path + paths = None + + #: dict of key -> value + _loaded = None + + def __init__(self, *args, **kwds): + self.paths = {} + self._loaded = {} + super(WordsetDict, self).__init__(*args, **kwds) + + def __getitem__(self, key): + try: + return self._loaded[key] + except KeyError: + pass + path = self.paths[key] + value = self._loaded[key] = _load_wordset(path) + return value + + def set_path(self, key, path): + """ + set asset path to lazy-load wordset from. + """ + self.paths[key] = path + + def __setitem__(self, key, value): + self._loaded[key] = value + + def __delitem__(self, key): + if key in self: + del self._loaded[key] + self.paths.pop(key, None) + else: + del self.paths[key] + + @property + def _keyset(self): + keys = set(self._loaded) + keys.update(self.paths) + return keys + + def __iter__(self): + return iter(self._keyset) + + def __len__(self): + return len(self._keyset) + + # NOTE: speeds things up, and prevents contains from lazy-loading + def __contains__(self, key): + return key in self._loaded or key in self.paths + + +#: dict of predefined word sets. +#: key is name of wordset, value should be sequence of words. +default_wordsets = WordsetDict() + +# register the wordsets built into passlib +for name in "eff_long eff_short eff_prefixed bip39".split(): + default_wordsets.set_path(name, "passlib:_data/wordsets/%s.txt" % name) + +#============================================================================= +# passphrase generator +#============================================================================= +class PhraseGenerator(SequenceGenerator): + """class which generates passphrases by randomly choosing + from a list of unique words. + + :param wordset: + wordset to draw from. + :param preset: + name of preset wordlist to use instead of ``wordset``. + :param spaces: + whether to insert spaces between words in output (defaults to ``True``). + :param \\*\\*kwds: + all other keywords passed to the :class:`SequenceGenerator` parent class. + + .. autoattribute:: wordset + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: predefined wordset to use + wordset = "eff_long" + + #: list of words to draw from + words = None + + #: separator to use when joining words + sep = " " + + #============================================================================= + # init + #============================================================================= + def __init__(self, wordset=None, words=None, sep=None, **kwds): + + # load wordset + if words is not None: + if wordset is not None: + raise TypeError("`words` and `wordset` are mutually exclusive") + else: + if wordset is None: + wordset = self.wordset + assert wordset + words = default_wordsets[wordset] + self.wordset = wordset + + # init words + if not isinstance(words, _sequence_types): + words = tuple(words) + _ensure_unique(words, param="words") + self.words = words + + # init separator + if sep is None: + sep = self.sep + sep = to_unicode(sep, param="sep") + self.sep = sep + + # hand off to parent + super(PhraseGenerator, self).__init__(**kwds) + ##log.debug("PhraseGenerator(): entropy/word=%r entropy/char=%r min_chars=%r", + ## self.entropy_per_symbol, self.entropy_per_char, self.min_chars) + + #============================================================================= + # informational helpers + #============================================================================= + + @memoized_property + def symbol_count(self): + return len(self.words) + + #============================================================================= + # generation + #============================================================================= + + def __next__(self): + words = (self.rng.choice(self.words) for _ in irange(self.length)) + return self.sep.join(words) + + #============================================================================= + # eoc + #============================================================================= + + +def genphrase(entropy=None, length=None, returns=None, **kwds): + """Generate one or more random password / passphrases. + + This function uses :mod:`random.SystemRandom` to generate + one or more passwords; it can be configured to generate + alphanumeric passwords, or full english phrases. + The complexity of the password can be specified + by size, or by the desired amount of entropy. + + Usage Example:: + + >>> # generate random phrase with 48 bits of entropy + >>> from passlib import pwd + >>> pwd.genphrase() + 'gangly robbing salt shove' + + >>> # generate a random phrase with 52 bits of entropy + >>> # using a particular wordset + >>> pwd.genword(entropy=52, wordset="bip39") + 'wheat dilemma reward rescue diary' + + :param entropy: + Strength of resulting password, measured in 'guessing entropy' bits. + An appropriate **length** value will be calculated + based on the requested entropy amount, and the size of the word set. + + This can be a positive integer, or one of the following preset + strings: ``"weak"`` (24), ``"fair"`` (36), + ``"strong"`` (48), and ``"secure"`` (56). + + If neither this or **length** is specified, **entropy** will default + to ``"strong"`` (48). + + :param length: + Length of resulting password, measured in words. + If omitted, the size is auto-calculated based on the **entropy** parameter. + + If both **entropy** and **length** are specified, + the stronger value will be used. + + :param returns: + Controls what this function returns: + + * If ``None`` (the default), this function will generate a single password. + * If an integer, this function will return a list containing that many passwords. + * If the ``iter`` builtin, will return an iterator that yields passwords. + + :param words: + + Optionally specifies a list/set of words to use when randomly generating a passphrase. + This option cannot be combined with **wordset**. + + :param wordset: + + The predefined word set to draw from (if not specified by **words**). + There are currently four presets available: + + ``"eff_long"`` (the default) + + Wordset containing 7776 english words of ~7 letters. + Constructed by the EFF, it offers ~12.9 bits of entropy per word. + + This wordset (and the other ``"eff_"`` wordsets) + were `created by the EFF `_ + to aid in generating passwords. See their announcement page + for more details about the design & properties of these wordsets. + + ``"eff_short"`` + + Wordset containing 1296 english words of ~4.5 letters. + Constructed by the EFF, it offers ~10.3 bits of entropy per word. + + ``"eff_prefixed"`` + + Wordset containing 1296 english words of ~8 letters, + selected so that they each have a unique 3-character prefix. + Constructed by the EFF, it offers ~10.3 bits of entropy per word. + + ``"bip39"`` + + Wordset of 2048 english words of ~5 letters, + selected so that they each have a unique 4-character prefix. + Published as part of Bitcoin's `BIP 39 `_, + this wordset has exactly 11 bits of entropy per word. + + This list offers words that are typically shorter than ``"eff_long"`` + (at the cost of slightly less entropy); and much shorter than + ``"eff_prefixed"`` (at the cost of a longer unique prefix). + + :param sep: + Optional separator to use when joining words. + Defaults to ``" "`` (a space), but can be an empty string, a hyphen, etc. + + :returns: + :class:`!unicode` string containing randomly generated passphrase; + or list of 1+ passphrases if :samp:`returns={int}` is specified. + """ + gen = PhraseGenerator(entropy=entropy, length=length, **kwds) + return gen(returns) + +#============================================================================= +# strength measurement +# +# NOTE: +# for a little while, had rough draft of password strength measurement alg here. +# but not sure if there's value in yet another measurement algorithm, +# that's not just duplicating the effort of libraries like zxcbn. +# may revive it later, but for now, leaving some refs to others out there: +# * NIST 800-63 has simple alg +# * zxcvbn (https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation/) +# might also be good, and has approach similar to composite approach i was already thinking about, +# but much more well thought out. +# * passfault (https://github.com/c-a-m/passfault) looks thorough, +# but may have licensing issues, plus porting to python looks like very big job :( +# * give a look at running things through zlib - might be able to cheaply +# catch extra redundancies. +#============================================================================= + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/registry.py b/venv/Lib/site-packages/passlib/registry.py new file mode 100644 index 0000000..9964b25 --- /dev/null +++ b/venv/Lib/site-packages/passlib/registry.py @@ -0,0 +1,547 @@ +"""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 "" + + 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 +#=============================================================================