summaryrefslogtreecommitdiff
path: root/passlib/handlers/django.py
blob: b643a66afdd7a65f6ef3d4dc43cb9a5aa53bb422 (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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
"""passlib.handlers.django- Django password hash support"""
#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode
from hashlib import md5, sha1
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import to_unicode, classproperty
from passlib.utils.compat import b, bytes, str_to_uascii, uascii_to_str, unicode, u
from passlib.utils.pbkdf2 import pbkdf2
import passlib.utils.handlers as uh
# local
__all__ = [
    "django_salted_sha1",
    "django_salted_md5",
    "django_bcrypt",
    "django_pbkdf2_sha1",
    "django_pbkdf2_sha256",
    "django_des_crypt",
    "django_disabled",
]

#=============================================================================
# lazy imports & constants
#=============================================================================
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")

    min_salt_size = 0
        # 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

    @classproperty
    def _stub_checksum(cls):
        return cls.checksum_chars[0] * cls.checksum_size

    @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 or self._stub_checksum)

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 or self._stub_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.encrypt` 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.encrypt` 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())

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"

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.encrypt` 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.

    :type rounds: int
    :param rounds:
        Optional number of rounds to use.
        Defaults to 10000, 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 = 12000 # NOTE: using django default here
    _prf = "hmac-sha256"

    def _calc_checksum(self, secret):
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")
        hash = pbkdf2(secret, self.salt.encode("ascii"), self.rounds,
                      keylen=None, prf=self._prf)
        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.encrypt` 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.

    :type rounds: int
    :param rounds:
        Optional number of rounds to use.
        Defaults to 10000, 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
    _prf = "hmac-sha1"

#=============================================================================
# other
#=============================================================================
class django_des_crypt(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.encrypt` 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]``.

    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")
    ident = u("crypt$")
    checksum_chars = salt_chars = uh.HASH64_CHARS
    checksum_size = 11
    min_salt_size = default_salt_size = 2
    _stub_checksum = u('.')*11

    # 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 or self._stub_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()
        return des_crypt(salt=self.salt[:2])._calc_checksum(secret)

class django_disabled(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 generate 40-char alphanumeric string
        to each unusuable password. This class recognizes such strings,
        but for backwards compatibility, still returns ``"!"``.

    .. versionchanged:: 1.6.2 added Django 1.6 support
    """
    name = "django_disabled"

    @classmethod
    def identify(cls, hash):
        hash = uh.to_unicode_for_identify(hash)
        return hash.startswith(u("!"))

    def _calc_checksum(self, secret):
        return u("!")

    @classmethod
    def verify(cls, secret, hash):
        uh.validate_secret(secret)
        if not cls.identify(hash):
            raise uh.exc.InvalidHashError(cls)
        return False

#=============================================================================
# eof
#=============================================================================