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

This commit is contained in:
2026-07-02 18:49:15 +00:00
parent 7ab03d3298
commit 352f46e58c
5 changed files with 1599 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
"""passlib.handlers.digests - plain hash digests
"""
#=============================================================================
# imports
#=============================================================================
# core
import hashlib
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.utils import to_native_str, to_bytes, render_bytes, consteq
from passlib.utils.compat import unicode, str_to_uascii
import passlib.utils.handlers as uh
from passlib.crypto.digest import lookup_hash
# local
__all__ = [
"create_hex_hash",
"hex_md4",
"hex_md5",
"hex_sha1",
"hex_sha256",
"hex_sha512",
]
#=============================================================================
# helpers for hexadecimal hashes
#=============================================================================
class HexDigestHash(uh.StaticHandler):
"""this provides a template for supporting passwords stored as plain hexadecimal hashes"""
#===================================================================
# class attrs
#===================================================================
_hash_func = None # hash function to use - filled in by create_hex_hash()
checksum_size = None # filled in by create_hex_hash()
checksum_chars = uh.HEX_CHARS
#: special for detecting if _hash_func is just a stub method.
supported = True
#===================================================================
# methods
#===================================================================
@classmethod
def _norm_hash(cls, hash):
return hash.lower()
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return str_to_uascii(self._hash_func(secret).hexdigest())
#===================================================================
# eoc
#===================================================================
def create_hex_hash(digest, module=__name__, django_name=None, required=True):
"""
create hex-encoded unsalted hasher for specified digest algorithm.
.. versionchanged:: 1.7.3
If called with unknown/supported digest, won't throw error immediately,
but instead return a dummy hasher that will throw error when called.
set ``required=True`` to restore old behavior.
"""
info = lookup_hash(digest, required=required)
name = "hex_" + info.name
if not info.supported:
info.digest_size = 0
hasher = type(name, (HexDigestHash,), dict(
name=name,
__module__=module, # so ABCMeta won't clobber it
_hash_func=staticmethod(info.const), # sometimes it's a function, sometimes not. so wrap it.
checksum_size=info.digest_size*2,
__doc__="""This class implements a plain hexadecimal %s hash, and follows the :ref:`password-hash-api`.
It supports no optional or contextual keywords.
""" % (info.name,)
))
if not info.supported:
hasher.supported = False
if django_name:
hasher.django_name = django_name
return hasher
#=============================================================================
# predefined handlers
#=============================================================================
# NOTE: some digests below are marked as "required=False", because these may not be present on
# FIPS systems (see issue 116). if missing, will return stub hasher that throws error
# if an attempt is made to actually use hash/verify with them.
hex_md4 = create_hex_hash("md4", required=False)
hex_md5 = create_hex_hash("md5", django_name="unsalted_md5", required=False)
hex_sha1 = create_hex_hash("sha1", required=False)
hex_sha256 = create_hex_hash("sha256")
hex_sha512 = create_hex_hash("sha512")
#=============================================================================
# htdigest
#=============================================================================
class htdigest(uh.MinimalHandler):
"""htdigest hash function.
.. todo::
document this hash
"""
name = "htdigest"
setting_kwds = ()
context_kwds = ("user", "realm", "encoding")
default_encoding = "utf-8"
@classmethod
def hash(cls, secret, user, realm, encoding=None):
# NOTE: this was deliberately written so that raw bytes are passed through
# unchanged, the encoding kwd is only used to handle unicode values.
if not encoding:
encoding = cls.default_encoding
uh.validate_secret(secret)
if isinstance(secret, unicode):
secret = secret.encode(encoding)
user = to_bytes(user, encoding, "user")
realm = to_bytes(realm, encoding, "realm")
data = render_bytes("%s:%s:%s", user, realm, secret)
return hashlib.md5(data).hexdigest()
@classmethod
def _norm_hash(cls, hash):
"""normalize hash to native string, and validate it"""
hash = to_native_str(hash, param="hash")
if len(hash) != 32:
raise uh.exc.MalformedHashError(cls, "wrong size")
for char in hash:
if char not in uh.LC_HEX_CHARS:
raise uh.exc.MalformedHashError(cls, "invalid chars in hash")
return hash
@classmethod
def verify(cls, secret, hash, user, realm, encoding="utf-8"):
hash = cls._norm_hash(hash)
other = cls.hash(secret, user, realm, encoding)
return consteq(hash, other)
@classmethod
def identify(cls, hash):
try:
cls._norm_hash(hash)
except ValueError:
return False
return True
@uh.deprecated_method(deprecated="1.7", removed="2.0")
@classmethod
def genconfig(cls):
return cls.hash("", "", "")
@uh.deprecated_method(deprecated="1.7", removed="2.0")
@classmethod
def genhash(cls, secret, config, user, realm, encoding=None):
# NOTE: 'config' is ignored, as this hash has no salting / other configuration.
# just have to make sure it's valid.
cls._norm_hash(config)
return cls.hash(secret, user, realm, encoding)
#=============================================================================
# eof
#=============================================================================

View File

@@ -0,0 +1,512 @@
"""passlib.handlers.django- Django password hash support"""
#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode
from binascii import hexlify
from hashlib import md5, sha1, sha256
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.handlers.bcrypt import _wrapped_bcrypt
from passlib.hash import argon2, bcrypt, pbkdf2_sha1, pbkdf2_sha256
from passlib.utils import to_unicode, rng, getrandstr
from passlib.utils.binary import BASE64_CHARS
from passlib.utils.compat import str_to_uascii, uascii_to_str, unicode, u
from passlib.crypto.digest import pbkdf2_hmac
import passlib.utils.handlers as uh
# local
__all__ = [
"django_salted_sha1",
"django_salted_md5",
"django_bcrypt",
"django_pbkdf2_sha1",
"django_pbkdf2_sha256",
"django_argon2",
"django_des_crypt",
"django_disabled",
]
#=============================================================================
# lazy imports & constants
#=============================================================================
# imported by django_des_crypt._calc_checksum()
des_crypt = None
def _import_des_crypt():
global des_crypt
if des_crypt is None:
from passlib.hash import des_crypt
return des_crypt
# django 1.4's salt charset
SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
#=============================================================================
# salted hashes
#=============================================================================
class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler):
"""base class providing common code for django hashes"""
# name, ident, checksum_size must be set by subclass.
# ident must include "$" suffix.
setting_kwds = ("salt", "salt_size")
# NOTE: django 1.0-1.3 would accept empty salt strings.
# django 1.4 won't, but this appears to be regression
# (https://code.djangoproject.com/ticket/18144)
# so presumably it will be fixed in a later release.
default_salt_size = 12
max_salt_size = None
salt_chars = SALT_CHARS
checksum_chars = uh.LOWER_HEX_CHARS
@classmethod
def from_string(cls, hash):
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
return cls(salt=salt, checksum=chk)
def to_string(self):
return uh.render_mc2(self.ident, self.salt, self.checksum)
# NOTE: only used by PBKDF2
class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash):
"""base class providing common code for django hashes w/ variable rounds"""
setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",)
min_rounds = 1
@classmethod
def from_string(cls, hash):
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls)
return cls(rounds=rounds, salt=salt, checksum=chk)
def to_string(self):
return uh.render_mc3(self.ident, self.rounds, self.salt, self.checksum)
class django_salted_sha1(DjangoSaltedHash):
"""This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and uses a single round of SHA1.
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
This should be compatible with Django 1.4's :class:`!SHA1PasswordHasher` class.
.. versionchanged: 1.6
This class now generates 12-character salts instead of 5,
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
generates these hashes; but hashes generated in this manner will still be
correctly interpreted by earlier versions of Django.
"""
name = "django_salted_sha1"
django_name = "sha1"
ident = u("sha1$")
checksum_size = 40
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return str_to_uascii(sha1(self.salt.encode("ascii") + secret).hexdigest())
class django_salted_md5(DjangoSaltedHash):
"""This class implements Django's Salted MD5 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and uses a single round of MD5.
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
This should be compatible with the hashes generated by
Django 1.4's :class:`!MD5PasswordHasher` class.
.. versionchanged: 1.6
This class now generates 12-character salts instead of 5,
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
generates these hashes; but hashes generated in this manner will still be
correctly interpreted by earlier versions of Django.
"""
name = "django_salted_md5"
django_name = "md5"
ident = u("md5$")
checksum_size = 32
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest())
#=============================================================================
# BCrypt
#=============================================================================
django_bcrypt = uh.PrefixWrapper("django_bcrypt", bcrypt,
prefix=u('bcrypt$'), ident=u("bcrypt$"),
# NOTE: this docstring is duplicated in the docs, since sphinx
# seems to be having trouble reading it via autodata::
doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`.
This is identical to :class:`!bcrypt` itself, but with
the Django-specific prefix ``"bcrypt$"`` prepended.
See :doc:`/lib/passlib.hash.bcrypt` for more details,
the usage and behavior is identical.
This should be compatible with the hashes generated by
Django 1.4's :class:`!BCryptPasswordHasher` class.
.. versionadded:: 1.6
""")
django_bcrypt.django_name = "bcrypt"
django_bcrypt._using_clone_attrs += ("django_name",)
#=============================================================================
# BCRYPT + SHA256
#=============================================================================
class django_bcrypt_sha256(_wrapped_bcrypt):
"""This class implements Django 1.6's Bcrypt+SHA256 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
While the algorithm and format is somewhat different,
the api and options for this hash are identical to :class:`!bcrypt` itself,
see :doc:`bcrypt </lib/passlib.hash.bcrypt>` for more details.
.. versionadded:: 1.6.2
"""
name = "django_bcrypt_sha256"
django_name = "bcrypt_sha256"
_digest = sha256
# sample hash:
# bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
# XXX: we can't use .ident attr due to bcrypt code using it.
# working around that via django_prefix
django_prefix = u('bcrypt_sha256$')
@classmethod
def identify(cls, hash):
hash = uh.to_unicode_for_identify(hash)
if not hash:
return False
return hash.startswith(cls.django_prefix)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
if not hash.startswith(cls.django_prefix):
raise uh.exc.InvalidHashError(cls)
bhash = hash[len(cls.django_prefix):]
if not bhash.startswith("$2"):
raise uh.exc.MalformedHashError(cls)
return super(django_bcrypt_sha256, cls).from_string(bhash)
def to_string(self):
bhash = super(django_bcrypt_sha256, self).to_string()
return uascii_to_str(self.django_prefix) + bhash
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
secret = hexlify(self._digest(secret).digest())
return super(django_bcrypt_sha256, self)._calc_checksum(secret)
#=============================================================================
# PBKDF2 variants
#=============================================================================
class django_pbkdf2_sha256(DjangoVariableHash):
"""This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 29000, but must be within ``range(1,1<<32)``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
This should be compatible with the hashes generated by
Django 1.4's :class:`!PBKDF2PasswordHasher` class.
.. versionadded:: 1.6
"""
name = "django_pbkdf2_sha256"
django_name = "pbkdf2_sha256"
ident = u('pbkdf2_sha256$')
min_salt_size = 1
max_rounds = 0xffffffff # setting at 32-bit limit for now
checksum_chars = uh.PADDED_BASE64_CHARS
checksum_size = 44 # 32 bytes -> base64
default_rounds = pbkdf2_sha256.default_rounds # NOTE: django 1.6 uses 12000
_digest = "sha256"
def _calc_checksum(self, secret):
# NOTE: secret & salt will be encoded using UTF-8 by pbkdf2_hmac()
hash = pbkdf2_hmac(self._digest, secret, self.salt, self.rounds)
return b64encode(hash).rstrip().decode("ascii")
class django_pbkdf2_sha1(django_pbkdf2_sha256):
"""This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 131000, but must be within ``range(1,1<<32)``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
This should be compatible with the hashes generated by
Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class.
.. versionadded:: 1.6
"""
name = "django_pbkdf2_sha1"
django_name = "pbkdf2_sha1"
ident = u('pbkdf2_sha1$')
checksum_size = 28 # 20 bytes -> base64
default_rounds = pbkdf2_sha1.default_rounds # NOTE: django 1.6 uses 12000
_digest = "sha1"
#=============================================================================
# Argon2
#=============================================================================
# NOTE: as of 2019-11-11, Django's Argon2PasswordHasher only supports Type I;
# so limiting this to ensure that as well.
django_argon2 = uh.PrefixWrapper(
name="django_argon2",
wrapped=argon2.using(type="I"),
prefix=u('argon2'),
ident=u('argon2$argon2i$'),
# NOTE: this docstring is duplicated in the docs, since sphinx
# seems to be having trouble reading it via autodata::
doc="""This class implements Django 1.10's Argon2 wrapper, and follows the :ref:`password-hash-api`.
This is identical to :class:`!argon2` itself, but with
the Django-specific prefix ``"argon2$"`` prepended.
See :doc:`argon2 </lib/passlib.hash.argon2>` for more details,
the usage and behavior is identical.
This should be compatible with the hashes generated by
Django 1.10's :class:`!Argon2PasswordHasher` class.
.. versionadded:: 1.7
""")
django_argon2.django_name = "argon2"
django_argon2._using_clone_attrs += ("django_name",)
#=============================================================================
# DES
#=============================================================================
class django_des_crypt(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler):
"""This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`.
It supports a fixed-length salt.
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:param bool truncate_error:
By default, django_des_crypt will silently truncate passwords larger than 8 bytes.
Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
.. versionadded:: 1.7
This should be compatible with the hashes generated by
Django 1.4's :class:`!CryptPasswordHasher` class.
Note that Django only supports this hash on Unix systems
(though :class:`!django_des_crypt` is available cross-platform
under Passlib).
.. versionchanged:: 1.6
This class will now accept hashes with empty salt strings,
since Django 1.4 generates them this way.
"""
name = "django_des_crypt"
django_name = "crypt"
setting_kwds = ("salt", "salt_size", "truncate_error")
ident = u("crypt$")
checksum_chars = salt_chars = uh.HASH64_CHARS
checksum_size = 11
min_salt_size = default_salt_size = 2
truncate_size = 8
# NOTE: regarding duplicate salt field:
#
# django 1.0 had a "crypt$<salt1>$<salt2><digest>" hash format,
# used [a-z0-9] to generate a 5 char salt, stored it in salt1,
# duplicated the first two chars of salt1 as salt2.
# it would throw an error if salt1 was empty.
#
# django 1.4 started generating 2 char salt using the full alphabet,
# left salt1 empty, and only paid attention to salt2.
#
# in order to be compatible with django 1.0, the hashes generated
# by this function will always include salt1, unless the following
# class-level field is disabled (mainly used for testing)
use_duplicate_salt = True
@classmethod
def from_string(cls, hash):
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
if chk:
# chk should be full des_crypt hash
if not salt:
# django 1.4 always uses empty salt field,
# so extract salt from des_crypt hash <chk>
salt = chk[:2]
elif salt[:2] != chk[:2]:
# django 1.0 stored 5 chars in salt field, and duplicated
# the first two chars in <chk>. we keep the full salt,
# but make sure the first two chars match as sanity check.
raise uh.exc.MalformedHashError(cls,
"first two digits of salt and checksum must match")
# in all cases, strip salt chars from <chk>
chk = chk[2:]
return cls(salt=salt, checksum=chk)
def to_string(self):
salt = self.salt
chk = salt[:2] + self.checksum
if self.use_duplicate_salt:
# filling in salt field, so that we're compatible with django 1.0
return uh.render_mc2(self.ident, salt, chk)
else:
# django 1.4+ style hash
return uh.render_mc2(self.ident, "", chk)
def _calc_checksum(self, secret):
# NOTE: we lazily import des_crypt,
# since most django deploys won't use django_des_crypt
global des_crypt
if des_crypt is None:
_import_des_crypt()
# check for truncation (during .hash() calls only)
if self.use_defaults:
self._check_truncate_policy(secret)
return des_crypt(salt=self.salt[:2])._calc_checksum(secret)
class django_disabled(uh.ifc.DisabledHash, uh.StaticHandler):
"""This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`.
This class does not implement a hash, but instead
claims the special hash string ``"!"`` which Django uses
to indicate an account's password has been disabled.
* newly encrypted passwords will hash to ``"!"``.
* it rejects all passwords.
.. note::
Django 1.6 prepends a randomly generated 40-char alphanumeric string
to each unusuable password. This class recognizes such strings,
but for backwards compatibility, still returns ``"!"``.
See `<https://code.djangoproject.com/ticket/20079>`_ for why
Django appends an alphanumeric string.
.. versionchanged:: 1.6.2 added Django 1.6 support
.. versionchanged:: 1.7 started appending an alphanumeric string.
"""
name = "django_disabled"
_hash_prefix = u("!")
suffix_length = 40
# XXX: move this to StaticHandler, or wherever _hash_prefix is being used?
@classmethod
def identify(cls, hash):
hash = uh.to_unicode_for_identify(hash)
return hash.startswith(cls._hash_prefix)
def _calc_checksum(self, secret):
# generate random suffix to match django's behavior
return getrandstr(rng, BASE64_CHARS[:-2], self.suffix_length)
@classmethod
def verify(cls, secret, hash):
uh.validate_secret(secret)
if not cls.identify(hash):
raise uh.exc.InvalidHashError(cls)
return False
#=============================================================================
# eof
#=============================================================================

View File

@@ -0,0 +1,214 @@
"""passlib.handlers.fshp
"""
#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode, b64decode
import re
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.utils import to_unicode
import passlib.utils.handlers as uh
from passlib.utils.compat import bascii_to_str, iteritems, u,\
unicode
from passlib.crypto.digest import pbkdf1
# local
__all__ = [
'fshp',
]
#=============================================================================
# sha1-crypt
#=============================================================================
class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"""This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:param salt:
Optional raw salt string.
If not specified, one will be autogenerated (this is recommended).
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 16 bytes, but can be any non-negative value.
:param rounds:
Optional number of rounds to use.
Defaults to 480000, must be between 1 and 4294967295, inclusive.
:param variant:
Optionally specifies variant of FSHP to use.
* ``0`` - uses SHA-1 digest (deprecated).
* ``1`` - uses SHA-2/256 digest (default).
* ``2`` - uses SHA-2/384 digest.
* ``3`` - uses SHA-2/512 digest.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
#--GenericHandler--
name = "fshp"
setting_kwds = ("salt", "salt_size", "rounds", "variant")
checksum_chars = uh.PADDED_BASE64_CHARS
ident = u("{FSHP")
# checksum_size is property() that depends on variant
#--HasRawSalt--
default_salt_size = 16 # current passlib default, FSHP uses 8
max_salt_size = None
#--HasRounds--
# FIXME: should probably use different default rounds
# based on the variant. setting for default variant (sha256) for now.
default_rounds = 480000 # current passlib default, FSHP uses 4096
min_rounds = 1 # set by FSHP
max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP
rounds_cost = "linear"
#--variants--
default_variant = 1
_variant_info = {
# variant: (hash name, digest size)
0: ("sha1", 20),
1: ("sha256", 32),
2: ("sha384", 48),
3: ("sha512", 64),
}
_variant_aliases = dict(
[(unicode(k),k) for k in _variant_info] +
[(v[0],k) for k,v in iteritems(_variant_info)]
)
#===================================================================
# configuration
#===================================================================
@classmethod
def using(cls, variant=None, **kwds):
subcls = super(fshp, cls).using(**kwds)
if variant is not None:
subcls.default_variant = cls._norm_variant(variant)
return subcls
#===================================================================
# instance attrs
#===================================================================
variant = None
#===================================================================
# init
#===================================================================
def __init__(self, variant=None, **kwds):
# NOTE: variant must be set first, since it controls checksum size, etc.
self.use_defaults = kwds.get("use_defaults") # load this early
if variant is not None:
variant = self._norm_variant(variant)
elif self.use_defaults:
variant = self.default_variant
assert self._norm_variant(variant) == variant, "invalid default variant: %r" % (variant,)
else:
raise TypeError("no variant specified")
self.variant = variant
super(fshp, self).__init__(**kwds)
@classmethod
def _norm_variant(cls, variant):
if isinstance(variant, bytes):
variant = variant.decode("ascii")
if isinstance(variant, unicode):
try:
variant = cls._variant_aliases[variant]
except KeyError:
raise ValueError("invalid fshp variant")
if not isinstance(variant, int):
raise TypeError("fshp variant must be int or known alias")
if variant not in cls._variant_info:
raise ValueError("invalid fshp variant")
return variant
@property
def checksum_alg(self):
return self._variant_info[self.variant][0]
@property
def checksum_size(self):
return self._variant_info[self.variant][1]
#===================================================================
# formatting
#===================================================================
_hash_regex = re.compile(u(r"""
^
\{FSHP
(\d+)\| # variant
(\d+)\| # salt size
(\d+)\} # rounds
([a-zA-Z0-9+/]+={0,3}) # digest
$"""), re.X)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
variant, salt_size, rounds, data = m.group(1,2,3,4)
variant = int(variant)
salt_size = int(salt_size)
rounds = int(rounds)
try:
data = b64decode(data.encode("ascii"))
except TypeError:
raise uh.exc.MalformedHashError(cls)
salt = data[:salt_size]
chk = data[salt_size:]
return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant)
def to_string(self):
chk = self.checksum
salt = self.salt
data = bascii_to_str(b64encode(salt+chk))
return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data)
#===================================================================
# backend
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
# NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed.
# this has only a minimal impact on security,
# but it is worth noting this deviation.
return pbkdf1(
digest=self.checksum_alg,
secret=self.salt,
salt=secret,
rounds=self.rounds,
keylen=self.checksum_size,
)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

@@ -0,0 +1,359 @@
"""passlib.handlers.digests - plain hash digests
"""
#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode, b64decode
from hashlib import md5, sha1, sha256, sha512
import logging; log = logging.getLogger(__name__)
import re
# site
# pkg
from passlib.handlers.misc import plaintext
from passlib.utils import unix_crypt_schemes, to_unicode
from passlib.utils.compat import uascii_to_str, unicode, u
from passlib.utils.decor import classproperty
import passlib.utils.handlers as uh
# local
__all__ = [
"ldap_plaintext",
"ldap_md5",
"ldap_sha1",
"ldap_salted_md5",
"ldap_salted_sha1",
"ldap_salted_sha256",
"ldap_salted_sha512",
##"get_active_ldap_crypt_schemes",
"ldap_des_crypt",
"ldap_bsdi_crypt",
"ldap_md5_crypt",
"ldap_sha1_crypt",
"ldap_bcrypt",
"ldap_sha256_crypt",
"ldap_sha512_crypt",
]
#=============================================================================
# ldap helpers
#=============================================================================
class _Base64DigestHelper(uh.StaticHandler):
"""helper for ldap_md5 / ldap_sha1"""
# XXX: could combine this with hex digests in digests.py
ident = None # required - prefix identifier
_hash_func = None # required - hash function
_hash_regex = None # required - regexp to recognize hash
checksum_chars = uh.PADDED_BASE64_CHARS
@classproperty
def _hash_prefix(cls):
"""tell StaticHandler to strip ident from checksum"""
return cls.ident
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
chk = self._hash_func(secret).digest()
return b64encode(chk).decode("ascii")
class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"""helper for ldap_salted_md5 / ldap_salted_sha1"""
setting_kwds = ("salt", "salt_size")
checksum_chars = uh.PADDED_BASE64_CHARS
ident = None # required - prefix identifier
_hash_func = None # required - hash function
_hash_regex = None # required - regexp to recognize hash
min_salt_size = max_salt_size = 4
# NOTE: openldap implementation uses 4 byte salt,
# but it's been reported (issue 30) that some servers use larger salts.
# the semi-related rfc3112 recommends support for up to 16 byte salts.
min_salt_size = 4
default_salt_size = 4
max_salt_size = 16
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
try:
data = b64decode(m.group("tmp").encode("ascii"))
except TypeError:
raise uh.exc.MalformedHashError(cls)
cs = cls.checksum_size
assert cs
return cls(checksum=data[:cs], salt=data[cs:])
def to_string(self):
data = self.checksum + self.salt
hash = self.ident + b64encode(data).decode("ascii")
return uascii_to_str(hash)
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return self._hash_func(secret + self.salt).digest()
#=============================================================================
# implementations
#=============================================================================
class ldap_md5(_Base64DigestHelper):
"""This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`.
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
"""
name = "ldap_md5"
ident = u("{MD5}")
_hash_func = md5
_hash_regex = re.compile(u(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$"))
class ldap_sha1(_Base64DigestHelper):
"""This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`.
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
"""
name = "ldap_sha1"
ident = u("{SHA}")
_hash_func = sha1
_hash_regex = re.compile(u(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$"))
class ldap_salted_md5(_SaltedBase64DigestHelper):
"""This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`.
It supports a 4-16 byte salt.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: bytes
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it may be any 4-16 byte string.
:type salt_size: int
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 4 bytes for compatibility with the LDAP spec,
but some systems use larger salts, and Passlib supports
any value between 4-16.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
.. versionchanged:: 1.6
This format now supports variable length salts, instead of a fix 4 bytes.
"""
name = "ldap_salted_md5"
ident = u("{SMD5}")
checksum_size = 16
_hash_func = md5
_hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27,}={0,2})$"))
class ldap_salted_sha1(_SaltedBase64DigestHelper):
"""
This class stores passwords using LDAP's "Salted SHA1" format,
and follows the :ref:`password-hash-api`.
It supports a 4-16 byte salt.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: bytes
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it may be any 4-16 byte string.
:type salt_size: int
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 4 bytes for compatibility with the LDAP spec,
but some systems use larger salts, and Passlib supports
any value between 4-16.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
.. versionchanged:: 1.6
This format now supports variable length salts, instead of a fix 4 bytes.
"""
name = "ldap_salted_sha1"
ident = u("{SSHA}")
checksum_size = 20
_hash_func = sha1
# NOTE: 32 = ceil((20 + 4) * 4/3)
_hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32,}={0,2})$"))
class ldap_salted_sha256(_SaltedBase64DigestHelper):
"""
This class stores passwords using LDAP's "Salted SHA2-256" format,
and follows the :ref:`password-hash-api`.
It supports a 4-16 byte salt.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: bytes
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it may be any 4-16 byte string.
:type salt_size: int
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 8 bytes for compatibility with the LDAP spec,
but Passlib supports any value between 4-16.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.7.3
"""
name = "ldap_salted_sha256"
ident = u("{SSHA256}")
checksum_size = 32
default_salt_size = 8
_hash_func = sha256
# NOTE: 48 = ceil((32 + 4) * 4/3)
_hash_regex = re.compile(u(r"^\{SSHA256\}(?P<tmp>[+/a-zA-Z0-9]{48,}={0,2})$"))
class ldap_salted_sha512(_SaltedBase64DigestHelper):
"""
This class stores passwords using LDAP's "Salted SHA2-512" format,
and follows the :ref:`password-hash-api`.
It supports a 4-16 byte salt.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: bytes
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it may be any 4-16 byte string.
:type salt_size: int
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 8 bytes for compatibility with the LDAP spec,
but Passlib supports any value between 4-16.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.7.3
"""
name = "ldap_salted_sha512"
ident = u("{SSHA512}")
checksum_size = 64
default_salt_size = 8
_hash_func = sha512
# NOTE: 91 = ceil((64 + 4) * 4/3)
_hash_regex = re.compile(u(r"^\{SSHA512\}(?P<tmp>[+/a-zA-Z0-9]{91,}={0,2})$"))
class ldap_plaintext(plaintext):
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
This class acts much like the generic :class:`!passlib.hash.plaintext` handler,
except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix
used by RFC2307 passwords.
The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
following additional contextual keyword:
:type encoding: str
:param encoding:
This controls the character encoding to use (defaults to ``utf-8``).
This encoding will be used to encode :class:`!unicode` passwords
under Python 2, and decode :class:`!bytes` hashes under Python 3.
.. versionchanged:: 1.6
The ``encoding`` keyword was added.
"""
# NOTE: this subclasses plaintext, since all it does differently
# is override identify()
name = "ldap_plaintext"
_2307_pat = re.compile(u(r"^\{\w+\}.*$"))
@uh.deprecated_method(deprecated="1.7", removed="2.0")
@classmethod
def genconfig(cls):
# Overridding plaintext.genconfig() since it returns "",
# but have to return non-empty value due to identify() below
return "!"
@classmethod
def identify(cls, hash):
# NOTE: identifies all strings EXCEPT those with {XXX} prefix
hash = uh.to_unicode_for_identify(hash)
return bool(hash) and cls._2307_pat.match(hash) is None
#=============================================================================
# {CRYPT} wrappers
# the following are wrappers around the base crypt algorithms,
# which add the ldap required {CRYPT} prefix
#=============================================================================
ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ]
def _init_ldap_crypt_handlers():
# NOTE: I don't like to implicitly modify globals() like this,
# but don't want to write out all these handlers out either :)
g = globals()
for wname in unix_crypt_schemes:
name = 'ldap_' + wname
g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True)
del g
_init_ldap_crypt_handlers()
##_lcn_host = None
##def get_host_ldap_crypt_schemes():
## global _lcn_host
## if _lcn_host is None:
## from passlib.hosts import host_context
## schemes = host_context.schemes()
## _lcn_host = [
## "ldap_" + name
## for name in unix_crypt_names
## if name in schemes
## ]
## return _lcn_host
#=============================================================================
# eof
#=============================================================================

View File

@@ -0,0 +1,346 @@
"""passlib.handlers.md5_crypt - md5-crypt algorithm"""
#=============================================================================
# imports
#=============================================================================
# core
from hashlib import md5
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.utils import safe_crypt, test_crypt, repeat_string
from passlib.utils.binary import h64
from passlib.utils.compat import unicode, u
import passlib.utils.handlers as uh
# local
__all__ = [
"md5_crypt",
"apr_md5_crypt",
]
#=============================================================================
# pure-python backend
#=============================================================================
_BNULL = b"\x00"
_MD5_MAGIC = b"$1$"
_APR_MAGIC = b"$apr1$"
# pre-calculated offsets used to speed up C digest stage (see notes below).
# sequence generated using the following:
##perms_order = "p,pp,ps,psp,sp,spp".split(",")
##def offset(i):
## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") +
## ("p" if i % 7 else "") + ("" if i % 2 else "p"))
## return perms_order.index(key)
##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)]
_c_digest_offsets = (
(0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3),
(4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1),
(4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3),
)
# map used to transpose bytes when encoding final digest
_transpose_map = (12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11)
def _raw_md5_crypt(pwd, salt, use_apr=False):
"""perform raw md5-crypt calculation
this function provides a pure-python implementation of the internals
for the MD5-Crypt algorithms; it doesn't handle any of the
parsing/validation of the hash strings themselves.
:arg pwd: password chars/bytes to hash
:arg salt: salt chars to use
:arg use_apr: use apache variant
:returns:
encoded checksum chars
"""
# NOTE: regarding 'apr' format:
# really, apache? you had to invent a whole new "$apr1$" format,
# when all you did was change the ident incorporated into the hash?
# would love to find webpage explaining why just using a portable
# implementation of $1$ wasn't sufficient. *nothing else* was changed.
#===================================================================
# init & validate inputs
#===================================================================
# validate secret
# XXX: not sure what official unicode policy is, using this as default
if isinstance(pwd, unicode):
pwd = pwd.encode("utf-8")
assert isinstance(pwd, bytes), "pwd not unicode or bytes"
if _BNULL in pwd:
raise uh.exc.NullPasswordError(md5_crypt)
pwd_len = len(pwd)
# validate salt - should have been taken care of by caller
assert isinstance(salt, unicode), "salt not unicode"
salt = salt.encode("ascii")
assert len(salt) < 9, "salt too large"
# NOTE: spec says salts larger than 8 bytes should be truncated,
# instead of causing an error. this function assumes that's been
# taken care of by the handler class.
# load APR specific constants
if use_apr:
magic = _APR_MAGIC
else:
magic = _MD5_MAGIC
#===================================================================
# digest B - used as subinput to digest A
#===================================================================
db = md5(pwd + salt + pwd).digest()
#===================================================================
# digest A - used to initialize first round of digest C
#===================================================================
# start out with pwd + magic + salt
a_ctx = md5(pwd + magic + salt)
a_ctx_update = a_ctx.update
# add pwd_len bytes of b, repeating b as many times as needed.
a_ctx_update(repeat_string(db, pwd_len))
# add null chars & first char of password
# NOTE: this may have historically been a bug,
# where they meant to use db[0] instead of B_NULL,
# but the original code memclear'ed db,
# and now all implementations have to use this.
i = pwd_len
evenchar = pwd[:1]
while i:
a_ctx_update(_BNULL if i & 1 else evenchar)
i >>= 1
# finish A
da = a_ctx.digest()
#===================================================================
# digest C - for a 1000 rounds, combine A, S, and P
# digests in various ways; in order to burn CPU time.
#===================================================================
# NOTE: the original MD5-Crypt implementation performs the C digest
# calculation using the following loop:
#
##dc = da
##i = 0
##while i < rounds:
## tmp_ctx = md5(pwd if i & 1 else dc)
## if i % 3:
## tmp_ctx.update(salt)
## if i % 7:
## tmp_ctx.update(pwd)
## tmp_ctx.update(dc if i & 1 else pwd)
## dc = tmp_ctx.digest()
## i += 1
#
# The code Passlib uses (below) implements an equivalent algorithm,
# it's just been heavily optimized to pre-calculate a large number
# of things beforehand. It works off of a couple of observations
# about the original algorithm:
#
# 1. each round is a combination of 'dc', 'salt', and 'pwd'; and the exact
# combination is determined by whether 'i' a multiple of 2,3, and/or 7.
# 2. since lcm(2,3,7)==42, the series of combinations will repeat
# every 42 rounds.
# 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
# while odd rounds 1-41 consist of hash(round-specific-constant + dc)
#
# Using these observations, the following code...
# * calculates the round-specific combination of salt & pwd for each round 0-41
# * runs through as many 42-round blocks as possible (23)
# * runs through as many pairs of rounds as needed for remaining rounds (17)
# * this results in the required 42*23+2*17=1000 rounds required by md5_crypt.
#
# this cuts out a lot of the control overhead incurred when running the
# original loop 1000 times in python, resulting in ~20% increase in
# speed under CPython (though still 2x slower than glibc crypt)
# prepare the 6 combinations of pwd & salt which are needed
# (order of 'perms' must match how _c_digest_offsets was generated)
pwd_pwd = pwd+pwd
pwd_salt = pwd+salt
perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd]
# build up list of even-round & odd-round constants,
# and store in 21-element list as (even,odd) pairs.
data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]
# perform 23 blocks of 42 rounds each (for a total of 966 rounds)
dc = da
blocks = 23
while blocks:
for even, odd in data:
dc = md5(odd + md5(dc + even).digest()).digest()
blocks -= 1
# perform 17 more pairs of rounds (34 more rounds, for a total of 1000)
for even, odd in data[:17]:
dc = md5(odd + md5(dc + even).digest()).digest()
#===================================================================
# encode digest using appropriate transpose map
#===================================================================
return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii")
#=============================================================================
# handler
#=============================================================================
class _MD5_Common(uh.HasSalt, uh.GenericHandler):
"""common code for md5_crypt and apr_md5_crypt"""
#===================================================================
# class attrs
#===================================================================
# name - set in subclass
setting_kwds = ("salt", "salt_size")
# ident - set in subclass
checksum_size = 22
checksum_chars = uh.HASH64_CHARS
max_salt_size = 8
salt_chars = uh.HASH64_CHARS
#===================================================================
# methods
#===================================================================
@classmethod
def from_string(cls, hash):
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
return cls(salt=salt, checksum=chk)
def to_string(self):
return uh.render_mc2(self.ident, self.salt, self.checksum)
# _calc_checksum() - provided by subclass
#===================================================================
# eoc
#===================================================================
class md5_crypt(uh.HasManyBackends, _MD5_Common):
"""This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 8, but can be any value between 0 and 8.
(This is mainly needed when generating Cisco-compatible hashes,
which require ``salt_size=4``).
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
name = "md5_crypt"
ident = u("$1$")
#===================================================================
# methods
#===================================================================
# FIXME: can't find definitive policy on how md5-crypt handles non-ascii.
# all backends currently coerce -> utf-8
backends = ("os_crypt", "builtin")
#---------------------------------------------------------------
# os_crypt backend
#---------------------------------------------------------------
@classmethod
def _load_backend_os_crypt(cls):
if test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/'):
cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
return True
else:
return False
def _calc_checksum_os_crypt(self, secret):
config = self.ident + self.salt
hash = safe_crypt(secret, config)
if hash is None:
# py3's crypt.crypt() can't handle non-utf8 bytes.
# fallback to builtin alg, which is always available.
return self._calc_checksum_builtin(secret)
if not hash.startswith(config) or len(hash) != len(config) + 23:
raise uh.exc.CryptBackendError(self, config, hash)
return hash[-22:]
#---------------------------------------------------------------
# builtin backend
#---------------------------------------------------------------
@classmethod
def _load_backend_builtin(cls):
cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
return True
def _calc_checksum_builtin(self, secret):
return _raw_md5_crypt(secret, self.salt)
#===================================================================
# eoc
#===================================================================
class apr_md5_crypt(_MD5_Common):
"""This class implements the Apr-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt.
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
name = "apr_md5_crypt"
ident = u("$apr1$")
#===================================================================
# methods
#===================================================================
def _calc_checksum(self, secret):
return _raw_md5_crypt(secret, self.salt, use_apr=True)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================