summaryrefslogtreecommitdiff
path: root/passlib/handlers/digests.py
blob: 982155c9144a6ca8092f1419aa8942eec8c024d4 (plain)
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
#=============================================================================