Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/auth/hashers.py: 52%
376 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import base64
2import binascii
3import functools
4import hashlib
5import importlib
6import math
7import warnings
9from django.conf import settings
10from django.core.exceptions import ImproperlyConfigured
11from django.core.signals import setting_changed
12from django.dispatch import receiver
13from django.utils.crypto import (
14 RANDOM_STRING_CHARS,
15 constant_time_compare,
16 get_random_string,
17 pbkdf2,
18)
19from django.utils.module_loading import import_string
20from django.utils.translation import gettext_noop as _
22UNUSABLE_PASSWORD_PREFIX = "!" # This will never be a valid encoded hash
23UNUSABLE_PASSWORD_SUFFIX_LENGTH = (
24 40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
25)
28def is_password_usable(encoded):
29 """
30 Return True if this password wasn't generated by
31 User.set_unusable_password(), i.e. make_password(None).
32 """
33 return encoded is None or not encoded.startswith(UNUSABLE_PASSWORD_PREFIX)
36def check_password(password, encoded, setter=None, preferred="default"):
37 """
38 Return a boolean of whether the raw password matches the three
39 part encoded digest.
41 If setter is specified, it'll be called when you need to
42 regenerate the password.
43 """
44 if password is None or not is_password_usable(encoded): 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true
45 return False
47 preferred = get_hasher(preferred)
48 try:
49 hasher = identify_hasher(encoded)
50 except ValueError:
51 # encoded is gibberish or uses a hasher that's no longer installed.
52 return False
54 hasher_changed = hasher.algorithm != preferred.algorithm
55 must_update = hasher_changed or preferred.must_update(encoded)
56 is_correct = hasher.verify(password, encoded)
58 # If the hasher didn't change (we don't protect against enumeration if it
59 # does) and the password should get updated, try to close the timing gap
60 # between the work factor of the current encoded password and the default
61 # work factor.
62 if not is_correct and not hasher_changed and must_update: 62 ↛ 63line 62 didn't jump to line 63, because the condition on line 62 was never true
63 hasher.harden_runtime(password, encoded)
65 if setter and is_correct and must_update: 65 ↛ 66line 65 didn't jump to line 66, because the condition on line 65 was never true
66 setter(password)
67 return is_correct
70def make_password(password, salt=None, hasher="default"):
71 """
72 Turn a plain-text password into a hash for database storage
74 Same as encode() but generate a new random salt. If password is None then
75 return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
76 which disallows logins. Additional random string reduces chances of gaining
77 access to staff or superuser accounts. See ticket #20079 for more info.
78 """
79 if password is None:
80 return UNUSABLE_PASSWORD_PREFIX + get_random_string(
81 UNUSABLE_PASSWORD_SUFFIX_LENGTH
82 )
83 if not isinstance(password, (bytes, str)): 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true
84 raise TypeError(
85 "Password must be a string or bytes, got %s." % type(password).__qualname__
86 )
87 hasher = get_hasher(hasher)
88 salt = salt or hasher.salt()
89 return hasher.encode(password, salt)
92@functools.lru_cache()
93def get_hashers():
94 hashers = []
95 for hasher_path in settings.PASSWORD_HASHERS:
96 hasher_cls = import_string(hasher_path)
97 hasher = hasher_cls()
98 if not getattr(hasher, "algorithm"): 98 ↛ 99line 98 didn't jump to line 99, because the condition on line 98 was never true
99 raise ImproperlyConfigured(
100 "hasher doesn't specify an algorithm name: %s" % hasher_path
101 )
102 hashers.append(hasher)
103 return hashers
106@functools.lru_cache()
107def get_hashers_by_algorithm():
108 return {hasher.algorithm: hasher for hasher in get_hashers()}
111@receiver(setting_changed)
112def reset_hashers(**kwargs):
113 if kwargs["setting"] == "PASSWORD_HASHERS":
114 get_hashers.cache_clear()
115 get_hashers_by_algorithm.cache_clear()
118def get_hasher(algorithm="default"):
119 """
120 Return an instance of a loaded password hasher.
122 If algorithm is 'default', return the default hasher. Lazily import hashers
123 specified in the project's settings file if needed.
124 """
125 if hasattr(algorithm, "algorithm"): 125 ↛ 126line 125 didn't jump to line 126, because the condition on line 125 was never true
126 return algorithm
128 elif algorithm == "default":
129 return get_hashers()[0]
131 else:
132 hashers = get_hashers_by_algorithm()
133 try:
134 return hashers[algorithm]
135 except KeyError:
136 raise ValueError(
137 "Unknown password hashing algorithm '%s'. "
138 "Did you specify it in the PASSWORD_HASHERS "
139 "setting?" % algorithm
140 )
143def identify_hasher(encoded):
144 """
145 Return an instance of a loaded password hasher.
147 Identify hasher algorithm by examining encoded hash, and call
148 get_hasher() to return hasher. Raise ValueError if
149 algorithm cannot be identified, or if hasher is not loaded.
150 """
151 # Ancient versions of Django created plain MD5 passwords and accepted
152 # MD5 passwords with an empty salt.
153 if (len(encoded) == 32 and "$" not in encoded) or ( 153 ↛ 156line 153 didn't jump to line 156, because the condition on line 153 was never true
154 len(encoded) == 37 and encoded.startswith("md5$$")
155 ):
156 algorithm = "unsalted_md5"
157 # Ancient versions of Django accepted SHA1 passwords with an empty salt.
158 elif len(encoded) == 46 and encoded.startswith("sha1$$"): 158 ↛ 159line 158 didn't jump to line 159, because the condition on line 158 was never true
159 algorithm = "unsalted_sha1"
160 else:
161 algorithm = encoded.split("$", 1)[0]
162 return get_hasher(algorithm)
165def mask_hash(hash, show=6, char="*"):
166 """
167 Return the given hash, with only the first ``show`` number shown. The
168 rest are masked with ``char`` for security reasons.
169 """
170 masked = hash[:show]
171 masked += char * len(hash[show:])
172 return masked
175def must_update_salt(salt, expected_entropy):
176 # Each character in the salt provides log_2(len(alphabet)) bits of entropy.
177 return len(salt) * math.log2(len(RANDOM_STRING_CHARS)) < expected_entropy
180class BasePasswordHasher:
181 """
182 Abstract base class for password hashers
184 When creating your own hasher, you need to override algorithm,
185 verify(), encode() and safe_summary().
187 PasswordHasher objects are immutable.
188 """
190 algorithm = None
191 library = None
192 salt_entropy = 128
194 def _load_library(self):
195 if self.library is not None:
196 if isinstance(self.library, (tuple, list)):
197 name, mod_path = self.library
198 else:
199 mod_path = self.library
200 try:
201 module = importlib.import_module(mod_path)
202 except ImportError as e:
203 raise ValueError(
204 "Couldn't load %r algorithm library: %s"
205 % (self.__class__.__name__, e)
206 )
207 return module
208 raise ValueError(
209 "Hasher %r doesn't specify a library attribute" % self.__class__.__name__
210 )
212 def salt(self):
213 """
214 Generate a cryptographically secure nonce salt in ASCII with an entropy
215 of at least `salt_entropy` bits.
216 """
217 # Each character in the salt provides
218 # log_2(len(alphabet)) bits of entropy.
219 char_count = math.ceil(self.salt_entropy / math.log2(len(RANDOM_STRING_CHARS)))
220 return get_random_string(char_count, allowed_chars=RANDOM_STRING_CHARS)
222 def verify(self, password, encoded):
223 """Check if the given password is correct."""
224 raise NotImplementedError(
225 "subclasses of BasePasswordHasher must provide a verify() method"
226 )
228 def _check_encode_args(self, password, salt):
229 if password is None: 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true
230 raise TypeError("password must be provided.")
231 if not salt or "$" in salt: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true
232 raise ValueError("salt must be provided and cannot contain $.")
234 def encode(self, password, salt):
235 """
236 Create an encoded database value.
238 The result is normally formatted as "algorithm$salt$hash" and
239 must be fewer than 128 characters.
240 """
241 raise NotImplementedError(
242 "subclasses of BasePasswordHasher must provide an encode() method"
243 )
245 def decode(self, encoded):
246 """
247 Return a decoded database value.
249 The result is a dictionary and should contain `algorithm`, `hash`, and
250 `salt`. Extra keys can be algorithm specific like `iterations` or
251 `work_factor`.
252 """
253 raise NotImplementedError(
254 "subclasses of BasePasswordHasher must provide a decode() method."
255 )
257 def safe_summary(self, encoded):
258 """
259 Return a summary of safe values.
261 The result is a dictionary and will be used where the password field
262 must be displayed to construct a safe representation of the password.
263 """
264 raise NotImplementedError(
265 "subclasses of BasePasswordHasher must provide a safe_summary() method"
266 )
268 def must_update(self, encoded):
269 return False
271 def harden_runtime(self, password, encoded):
272 """
273 Bridge the runtime gap between the work factor supplied in `encoded`
274 and the work factor suggested by this hasher.
276 Taking PBKDF2 as an example, if `encoded` contains 20000 iterations and
277 `self.iterations` is 30000, this method should run password through
278 another 10000 iterations of PBKDF2. Similar approaches should exist
279 for any hasher that has a work factor. If not, this method should be
280 defined as a no-op to silence the warning.
281 """
282 warnings.warn(
283 "subclasses of BasePasswordHasher should provide a harden_runtime() method"
284 )
287class PBKDF2PasswordHasher(BasePasswordHasher):
288 """
289 Secure password hashing using the PBKDF2 algorithm (recommended)
291 Configured to use PBKDF2 + HMAC + SHA256.
292 The result is a 64 byte binary string. Iterations may be changed
293 safely but you must rename the algorithm if you change SHA256.
294 """
296 algorithm = "pbkdf2_sha256"
297 iterations = 320000
298 digest = hashlib.sha256
300 def encode(self, password, salt, iterations=None):
301 self._check_encode_args(password, salt)
302 iterations = iterations or self.iterations
303 hash = pbkdf2(password, salt, iterations, digest=self.digest)
304 hash = base64.b64encode(hash).decode("ascii").strip()
305 return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
307 def decode(self, encoded):
308 algorithm, iterations, salt, hash = encoded.split("$", 3)
309 assert algorithm == self.algorithm
310 return {
311 "algorithm": algorithm,
312 "hash": hash,
313 "iterations": int(iterations),
314 "salt": salt,
315 }
317 def verify(self, password, encoded):
318 decoded = self.decode(encoded)
319 encoded_2 = self.encode(password, decoded["salt"], decoded["iterations"])
320 return constant_time_compare(encoded, encoded_2)
322 def safe_summary(self, encoded):
323 decoded = self.decode(encoded)
324 return {
325 _("algorithm"): decoded["algorithm"],
326 _("iterations"): decoded["iterations"],
327 _("salt"): mask_hash(decoded["salt"]),
328 _("hash"): mask_hash(decoded["hash"]),
329 }
331 def must_update(self, encoded):
332 decoded = self.decode(encoded)
333 update_salt = must_update_salt(decoded["salt"], self.salt_entropy)
334 return (decoded["iterations"] != self.iterations) or update_salt
336 def harden_runtime(self, password, encoded):
337 decoded = self.decode(encoded)
338 extra_iterations = self.iterations - decoded["iterations"]
339 if extra_iterations > 0:
340 self.encode(password, decoded["salt"], extra_iterations)
343class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
344 """
345 Alternate PBKDF2 hasher which uses SHA1, the default PRF
346 recommended by PKCS #5. This is compatible with other
347 implementations of PBKDF2, such as openssl's
348 PKCS5_PBKDF2_HMAC_SHA1().
349 """
351 algorithm = "pbkdf2_sha1"
352 digest = hashlib.sha1
355class Argon2PasswordHasher(BasePasswordHasher):
356 """
357 Secure password hashing using the argon2 algorithm.
359 This is the winner of the Password Hashing Competition 2013-2015
360 (https://password-hashing.net). It requires the argon2-cffi library which
361 depends on native C code and might cause portability issues.
362 """
364 algorithm = "argon2"
365 library = "argon2"
367 time_cost = 2
368 memory_cost = 102400
369 parallelism = 8
371 def encode(self, password, salt):
372 argon2 = self._load_library()
373 params = self.params()
374 data = argon2.low_level.hash_secret(
375 password.encode(),
376 salt.encode(),
377 time_cost=params.time_cost,
378 memory_cost=params.memory_cost,
379 parallelism=params.parallelism,
380 hash_len=params.hash_len,
381 type=params.type,
382 )
383 return self.algorithm + data.decode("ascii")
385 def decode(self, encoded):
386 argon2 = self._load_library()
387 algorithm, rest = encoded.split("$", 1)
388 assert algorithm == self.algorithm
389 params = argon2.extract_parameters("$" + rest)
390 variety, *_, b64salt, hash = rest.split("$")
391 # Add padding.
392 b64salt += "=" * (-len(b64salt) % 4)
393 salt = base64.b64decode(b64salt).decode("latin1")
394 return {
395 "algorithm": algorithm,
396 "hash": hash,
397 "memory_cost": params.memory_cost,
398 "parallelism": params.parallelism,
399 "salt": salt,
400 "time_cost": params.time_cost,
401 "variety": variety,
402 "version": params.version,
403 "params": params,
404 }
406 def verify(self, password, encoded):
407 argon2 = self._load_library()
408 algorithm, rest = encoded.split("$", 1)
409 assert algorithm == self.algorithm
410 try:
411 return argon2.PasswordHasher().verify("$" + rest, password)
412 except argon2.exceptions.VerificationError:
413 return False
415 def safe_summary(self, encoded):
416 decoded = self.decode(encoded)
417 return {
418 _("algorithm"): decoded["algorithm"],
419 _("variety"): decoded["variety"],
420 _("version"): decoded["version"],
421 _("memory cost"): decoded["memory_cost"],
422 _("time cost"): decoded["time_cost"],
423 _("parallelism"): decoded["parallelism"],
424 _("salt"): mask_hash(decoded["salt"]),
425 _("hash"): mask_hash(decoded["hash"]),
426 }
428 def must_update(self, encoded):
429 decoded = self.decode(encoded)
430 current_params = decoded["params"]
431 new_params = self.params()
432 # Set salt_len to the salt_len of the current parameters because salt
433 # is explicitly passed to argon2.
434 new_params.salt_len = current_params.salt_len
435 update_salt = must_update_salt(decoded["salt"], self.salt_entropy)
436 return (current_params != new_params) or update_salt
438 def harden_runtime(self, password, encoded):
439 # The runtime for Argon2 is too complicated to implement a sensible
440 # hardening algorithm.
441 pass
443 def params(self):
444 argon2 = self._load_library()
445 # salt_len is a noop, because we provide our own salt.
446 return argon2.Parameters(
447 type=argon2.low_level.Type.ID,
448 version=argon2.low_level.ARGON2_VERSION,
449 salt_len=argon2.DEFAULT_RANDOM_SALT_LENGTH,
450 hash_len=argon2.DEFAULT_HASH_LENGTH,
451 time_cost=self.time_cost,
452 memory_cost=self.memory_cost,
453 parallelism=self.parallelism,
454 )
457class BCryptSHA256PasswordHasher(BasePasswordHasher):
458 """
459 Secure password hashing using the bcrypt algorithm (recommended)
461 This is considered by many to be the most secure algorithm but you
462 must first install the bcrypt library. Please be warned that
463 this library depends on native C code and might cause portability
464 issues.
465 """
467 algorithm = "bcrypt_sha256"
468 digest = hashlib.sha256
469 library = ("bcrypt", "bcrypt")
470 rounds = 12
472 def salt(self):
473 bcrypt = self._load_library()
474 return bcrypt.gensalt(self.rounds)
476 def encode(self, password, salt):
477 bcrypt = self._load_library()
478 password = password.encode()
479 # Hash the password prior to using bcrypt to prevent password
480 # truncation as described in #20138.
481 if self.digest is not None:
482 # Use binascii.hexlify() because a hex encoded bytestring is str.
483 password = binascii.hexlify(self.digest(password).digest())
485 data = bcrypt.hashpw(password, salt)
486 return "%s$%s" % (self.algorithm, data.decode("ascii"))
488 def decode(self, encoded):
489 algorithm, empty, algostr, work_factor, data = encoded.split("$", 4)
490 assert algorithm == self.algorithm
491 return {
492 "algorithm": algorithm,
493 "algostr": algostr,
494 "checksum": data[22:],
495 "salt": data[:22],
496 "work_factor": int(work_factor),
497 }
499 def verify(self, password, encoded):
500 algorithm, data = encoded.split("$", 1)
501 assert algorithm == self.algorithm
502 encoded_2 = self.encode(password, data.encode("ascii"))
503 return constant_time_compare(encoded, encoded_2)
505 def safe_summary(self, encoded):
506 decoded = self.decode(encoded)
507 return {
508 _("algorithm"): decoded["algorithm"],
509 _("work factor"): decoded["work_factor"],
510 _("salt"): mask_hash(decoded["salt"]),
511 _("checksum"): mask_hash(decoded["checksum"]),
512 }
514 def must_update(self, encoded):
515 decoded = self.decode(encoded)
516 return decoded["work_factor"] != self.rounds
518 def harden_runtime(self, password, encoded):
519 _, data = encoded.split("$", 1)
520 salt = data[:29] # Length of the salt in bcrypt.
521 rounds = data.split("$")[2]
522 # work factor is logarithmic, adding one doubles the load.
523 diff = 2 ** (self.rounds - int(rounds)) - 1
524 while diff > 0:
525 self.encode(password, salt.encode("ascii"))
526 diff -= 1
529class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
530 """
531 Secure password hashing using the bcrypt algorithm
533 This is considered by many to be the most secure algorithm but you
534 must first install the bcrypt library. Please be warned that
535 this library depends on native C code and might cause portability
536 issues.
538 This hasher does not first hash the password which means it is subject to
539 bcrypt's 72 bytes password truncation. Most use cases should prefer the
540 BCryptSHA256PasswordHasher.
541 """
543 algorithm = "bcrypt"
544 digest = None
547class ScryptPasswordHasher(BasePasswordHasher):
548 """
549 Secure password hashing using the Scrypt algorithm.
550 """
552 algorithm = "scrypt"
553 block_size = 8
554 maxmem = 0
555 parallelism = 1
556 work_factor = 2**14
558 def encode(self, password, salt, n=None, r=None, p=None):
559 self._check_encode_args(password, salt)
560 n = n or self.work_factor
561 r = r or self.block_size
562 p = p or self.parallelism
563 hash_ = hashlib.scrypt(
564 password.encode(),
565 salt=salt.encode(),
566 n=n,
567 r=r,
568 p=p,
569 maxmem=self.maxmem,
570 dklen=64,
571 )
572 hash_ = base64.b64encode(hash_).decode("ascii").strip()
573 return "%s$%d$%s$%d$%d$%s" % (self.algorithm, n, salt, r, p, hash_)
575 def decode(self, encoded):
576 algorithm, work_factor, salt, block_size, parallelism, hash_ = encoded.split(
577 "$", 6
578 )
579 assert algorithm == self.algorithm
580 return {
581 "algorithm": algorithm,
582 "work_factor": int(work_factor),
583 "salt": salt,
584 "block_size": int(block_size),
585 "parallelism": int(parallelism),
586 "hash": hash_,
587 }
589 def verify(self, password, encoded):
590 decoded = self.decode(encoded)
591 encoded_2 = self.encode(
592 password,
593 decoded["salt"],
594 decoded["work_factor"],
595 decoded["block_size"],
596 decoded["parallelism"],
597 )
598 return constant_time_compare(encoded, encoded_2)
600 def safe_summary(self, encoded):
601 decoded = self.decode(encoded)
602 return {
603 _("algorithm"): decoded["algorithm"],
604 _("work factor"): decoded["work_factor"],
605 _("block size"): decoded["block_size"],
606 _("parallelism"): decoded["parallelism"],
607 _("salt"): mask_hash(decoded["salt"]),
608 _("hash"): mask_hash(decoded["hash"]),
609 }
611 def must_update(self, encoded):
612 decoded = self.decode(encoded)
613 return (
614 decoded["work_factor"] != self.work_factor
615 or decoded["block_size"] != self.block_size
616 or decoded["parallelism"] != self.parallelism
617 )
619 def harden_runtime(self, password, encoded):
620 # The runtime for Scrypt is too complicated to implement a sensible
621 # hardening algorithm.
622 pass
625class SHA1PasswordHasher(BasePasswordHasher):
626 """
627 The SHA1 password hashing algorithm (not recommended)
628 """
630 algorithm = "sha1"
632 def encode(self, password, salt):
633 self._check_encode_args(password, salt)
634 hash = hashlib.sha1((salt + password).encode()).hexdigest()
635 return "%s$%s$%s" % (self.algorithm, salt, hash)
637 def decode(self, encoded):
638 algorithm, salt, hash = encoded.split("$", 2)
639 assert algorithm == self.algorithm
640 return {
641 "algorithm": algorithm,
642 "hash": hash,
643 "salt": salt,
644 }
646 def verify(self, password, encoded):
647 decoded = self.decode(encoded)
648 encoded_2 = self.encode(password, decoded["salt"])
649 return constant_time_compare(encoded, encoded_2)
651 def safe_summary(self, encoded):
652 decoded = self.decode(encoded)
653 return {
654 _("algorithm"): decoded["algorithm"],
655 _("salt"): mask_hash(decoded["salt"], show=2),
656 _("hash"): mask_hash(decoded["hash"]),
657 }
659 def must_update(self, encoded):
660 decoded = self.decode(encoded)
661 return must_update_salt(decoded["salt"], self.salt_entropy)
663 def harden_runtime(self, password, encoded):
664 pass
667class MD5PasswordHasher(BasePasswordHasher):
668 """
669 The Salted MD5 password hashing algorithm (not recommended)
670 """
672 algorithm = "md5"
674 def encode(self, password, salt):
675 self._check_encode_args(password, salt)
676 hash = hashlib.md5((salt + password).encode()).hexdigest()
677 return "%s$%s$%s" % (self.algorithm, salt, hash)
679 def decode(self, encoded):
680 algorithm, salt, hash = encoded.split("$", 2)
681 assert algorithm == self.algorithm
682 return {
683 "algorithm": algorithm,
684 "hash": hash,
685 "salt": salt,
686 }
688 def verify(self, password, encoded):
689 decoded = self.decode(encoded)
690 encoded_2 = self.encode(password, decoded["salt"])
691 return constant_time_compare(encoded, encoded_2)
693 def safe_summary(self, encoded):
694 decoded = self.decode(encoded)
695 return {
696 _("algorithm"): decoded["algorithm"],
697 _("salt"): mask_hash(decoded["salt"], show=2),
698 _("hash"): mask_hash(decoded["hash"]),
699 }
701 def must_update(self, encoded):
702 decoded = self.decode(encoded)
703 return must_update_salt(decoded["salt"], self.salt_entropy)
705 def harden_runtime(self, password, encoded):
706 pass
709class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
710 """
711 Very insecure algorithm that you should *never* use; store SHA1 hashes
712 with an empty salt.
714 This class is implemented because Django used to accept such password
715 hashes. Some older Django installs still have these values lingering
716 around so we need to handle and upgrade them properly.
717 """
719 algorithm = "unsalted_sha1"
721 def salt(self):
722 return ""
724 def encode(self, password, salt):
725 if salt != "":
726 raise ValueError("salt must be empty.")
727 hash = hashlib.sha1(password.encode()).hexdigest()
728 return "sha1$$%s" % hash
730 def decode(self, encoded):
731 assert encoded.startswith("sha1$$")
732 return {
733 "algorithm": self.algorithm,
734 "hash": encoded[6:],
735 "salt": None,
736 }
738 def verify(self, password, encoded):
739 encoded_2 = self.encode(password, "")
740 return constant_time_compare(encoded, encoded_2)
742 def safe_summary(self, encoded):
743 decoded = self.decode(encoded)
744 return {
745 _("algorithm"): decoded["algorithm"],
746 _("hash"): mask_hash(decoded["hash"]),
747 }
749 def harden_runtime(self, password, encoded):
750 pass
753class UnsaltedMD5PasswordHasher(BasePasswordHasher):
754 """
755 Incredibly insecure algorithm that you should *never* use; stores unsalted
756 MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an
757 empty salt.
759 This class is implemented because Django used to store passwords this way
760 and to accept such password hashes. Some older Django installs still have
761 these values lingering around so we need to handle and upgrade them
762 properly.
763 """
765 algorithm = "unsalted_md5"
767 def salt(self):
768 return ""
770 def encode(self, password, salt):
771 if salt != "":
772 raise ValueError("salt must be empty.")
773 return hashlib.md5(password.encode()).hexdigest()
775 def decode(self, encoded):
776 return {
777 "algorithm": self.algorithm,
778 "hash": encoded,
779 "salt": None,
780 }
782 def verify(self, password, encoded):
783 if len(encoded) == 37 and encoded.startswith("md5$$"):
784 encoded = encoded[5:]
785 encoded_2 = self.encode(password, "")
786 return constant_time_compare(encoded, encoded_2)
788 def safe_summary(self, encoded):
789 decoded = self.decode(encoded)
790 return {
791 _("algorithm"): decoded["algorithm"],
792 _("hash"): mask_hash(decoded["hash"], show=3),
793 }
795 def harden_runtime(self, password, encoded):
796 pass
799class CryptPasswordHasher(BasePasswordHasher):
800 """
801 Password hashing using UNIX crypt (not recommended)
803 The crypt module is not supported on all platforms.
804 """
806 algorithm = "crypt"
807 library = "crypt"
809 def salt(self):
810 return get_random_string(2)
812 def encode(self, password, salt):
813 crypt = self._load_library()
814 if len(salt) != 2:
815 raise ValueError("salt must be of length 2.")
816 hash = crypt.crypt(password, salt)
817 if hash is None: # A platform like OpenBSD with a dummy crypt module.
818 raise TypeError("hash must be provided.")
819 # we don't need to store the salt, but Django used to do this
820 return "%s$%s$%s" % (self.algorithm, "", hash)
822 def decode(self, encoded):
823 algorithm, salt, hash = encoded.split("$", 2)
824 assert algorithm == self.algorithm
825 return {
826 "algorithm": algorithm,
827 "hash": hash,
828 "salt": salt,
829 }
831 def verify(self, password, encoded):
832 crypt = self._load_library()
833 decoded = self.decode(encoded)
834 data = crypt.crypt(password, decoded["hash"])
835 return constant_time_compare(decoded["hash"], data)
837 def safe_summary(self, encoded):
838 decoded = self.decode(encoded)
839 return {
840 _("algorithm"): decoded["algorithm"],
841 _("salt"): decoded["salt"],
842 _("hash"): mask_hash(decoded["hash"], show=3),
843 }
845 def harden_runtime(self, password, encoded):
846 pass