summaryrefslogtreecommitdiff
path: root/passlib/handlers/django.py
blob: 6dd499ac215d56cad4e39155d4621c4c4e554c3f (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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
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
#=============================================================================