1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
"""plain hash digests"""
import hashlib
import passlib.utils.handlers as uh
from passlib.crypto.digest import lookup_hash
from passlib.utils import consteq, render_bytes, to_bytes, to_native_str
__all__ = [
"create_hex_hash",
"hex_md4",
"hex_md5",
"hex_sha1",
"hex_sha256",
"hex_sha512",
]
class HexDigestHash(uh.StaticHandler):
"""this provides a template for supporting passwords stored as plain hexadecimal hashes"""
_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
@classmethod
def _norm_hash(cls, hash):
return hash.lower()
def _calc_checksum(self, secret):
if isinstance(secret, str):
secret = secret.encode("utf-8")
return self._hash_func(secret).hexdigest()
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__=f"""This class implements a plain hexadecimal {info.name} hash, and follows the :ref:`password-hash-api`.
It supports no optional or contextual keywords.
""",
),
)
if not info.supported:
hasher.supported = False
if django_name:
hasher.django_name = django_name
return hasher
# 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")
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, str):
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)
|