Загрузить файлы в «venv/Lib/site-packages/passlib»

This commit is contained in:
2026-07-02 18:53:58 +00:00
parent 2d6a2bd435
commit fdbd09ed51
5 changed files with 1883 additions and 0 deletions

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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, "<None>, int, or <iter>", "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 "<int> <word>" 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 <https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases>`_
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 <https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt>`_,
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
#=============================================================================

View File

@@ -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 "<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
#=============================================================================