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

1import base64 

2import binascii 

3import functools 

4import hashlib 

5import importlib 

6import math 

7import warnings 

8 

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 _ 

21 

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) 

26 

27 

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) 

34 

35 

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. 

40 

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 

46 

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 

53 

54 hasher_changed = hasher.algorithm != preferred.algorithm 

55 must_update = hasher_changed or preferred.must_update(encoded) 

56 is_correct = hasher.verify(password, encoded) 

57 

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) 

64 

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 

68 

69 

70def make_password(password, salt=None, hasher="default"): 

71 """ 

72 Turn a plain-text password into a hash for database storage 

73 

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) 

90 

91 

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 

104 

105 

106@functools.lru_cache() 

107def get_hashers_by_algorithm(): 

108 return {hasher.algorithm: hasher for hasher in get_hashers()} 

109 

110 

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() 

116 

117 

118def get_hasher(algorithm="default"): 

119 """ 

120 Return an instance of a loaded password hasher. 

121 

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 

127 

128 elif algorithm == "default": 

129 return get_hashers()[0] 

130 

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 ) 

141 

142 

143def identify_hasher(encoded): 

144 """ 

145 Return an instance of a loaded password hasher. 

146 

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) 

163 

164 

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 

173 

174 

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 

178 

179 

180class BasePasswordHasher: 

181 """ 

182 Abstract base class for password hashers 

183 

184 When creating your own hasher, you need to override algorithm, 

185 verify(), encode() and safe_summary(). 

186 

187 PasswordHasher objects are immutable. 

188 """ 

189 

190 algorithm = None 

191 library = None 

192 salt_entropy = 128 

193 

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 ) 

211 

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) 

221 

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 ) 

227 

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 $.") 

233 

234 def encode(self, password, salt): 

235 """ 

236 Create an encoded database value. 

237 

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 ) 

244 

245 def decode(self, encoded): 

246 """ 

247 Return a decoded database value. 

248 

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 ) 

256 

257 def safe_summary(self, encoded): 

258 """ 

259 Return a summary of safe values. 

260 

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 ) 

267 

268 def must_update(self, encoded): 

269 return False 

270 

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. 

275 

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 ) 

285 

286 

287class PBKDF2PasswordHasher(BasePasswordHasher): 

288 """ 

289 Secure password hashing using the PBKDF2 algorithm (recommended) 

290 

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 """ 

295 

296 algorithm = "pbkdf2_sha256" 

297 iterations = 320000 

298 digest = hashlib.sha256 

299 

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) 

306 

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 } 

316 

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) 

321 

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 } 

330 

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 

335 

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) 

341 

342 

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 """ 

350 

351 algorithm = "pbkdf2_sha1" 

352 digest = hashlib.sha1 

353 

354 

355class Argon2PasswordHasher(BasePasswordHasher): 

356 """ 

357 Secure password hashing using the argon2 algorithm. 

358 

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 """ 

363 

364 algorithm = "argon2" 

365 library = "argon2" 

366 

367 time_cost = 2 

368 memory_cost = 102400 

369 parallelism = 8 

370 

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") 

384 

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 } 

405 

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 

414 

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 } 

427 

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 

437 

438 def harden_runtime(self, password, encoded): 

439 # The runtime for Argon2 is too complicated to implement a sensible 

440 # hardening algorithm. 

441 pass 

442 

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 ) 

455 

456 

457class BCryptSHA256PasswordHasher(BasePasswordHasher): 

458 """ 

459 Secure password hashing using the bcrypt algorithm (recommended) 

460 

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 """ 

466 

467 algorithm = "bcrypt_sha256" 

468 digest = hashlib.sha256 

469 library = ("bcrypt", "bcrypt") 

470 rounds = 12 

471 

472 def salt(self): 

473 bcrypt = self._load_library() 

474 return bcrypt.gensalt(self.rounds) 

475 

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()) 

484 

485 data = bcrypt.hashpw(password, salt) 

486 return "%s$%s" % (self.algorithm, data.decode("ascii")) 

487 

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 } 

498 

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) 

504 

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 } 

513 

514 def must_update(self, encoded): 

515 decoded = self.decode(encoded) 

516 return decoded["work_factor"] != self.rounds 

517 

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 

527 

528 

529class BCryptPasswordHasher(BCryptSHA256PasswordHasher): 

530 """ 

531 Secure password hashing using the bcrypt algorithm 

532 

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. 

537 

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 """ 

542 

543 algorithm = "bcrypt" 

544 digest = None 

545 

546 

547class ScryptPasswordHasher(BasePasswordHasher): 

548 """ 

549 Secure password hashing using the Scrypt algorithm. 

550 """ 

551 

552 algorithm = "scrypt" 

553 block_size = 8 

554 maxmem = 0 

555 parallelism = 1 

556 work_factor = 2**14 

557 

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_) 

574 

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 } 

588 

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) 

599 

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 } 

610 

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 ) 

618 

619 def harden_runtime(self, password, encoded): 

620 # The runtime for Scrypt is too complicated to implement a sensible 

621 # hardening algorithm. 

622 pass 

623 

624 

625class SHA1PasswordHasher(BasePasswordHasher): 

626 """ 

627 The SHA1 password hashing algorithm (not recommended) 

628 """ 

629 

630 algorithm = "sha1" 

631 

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) 

636 

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 } 

645 

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) 

650 

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 } 

658 

659 def must_update(self, encoded): 

660 decoded = self.decode(encoded) 

661 return must_update_salt(decoded["salt"], self.salt_entropy) 

662 

663 def harden_runtime(self, password, encoded): 

664 pass 

665 

666 

667class MD5PasswordHasher(BasePasswordHasher): 

668 """ 

669 The Salted MD5 password hashing algorithm (not recommended) 

670 """ 

671 

672 algorithm = "md5" 

673 

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) 

678 

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 } 

687 

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) 

692 

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 } 

700 

701 def must_update(self, encoded): 

702 decoded = self.decode(encoded) 

703 return must_update_salt(decoded["salt"], self.salt_entropy) 

704 

705 def harden_runtime(self, password, encoded): 

706 pass 

707 

708 

709class UnsaltedSHA1PasswordHasher(BasePasswordHasher): 

710 """ 

711 Very insecure algorithm that you should *never* use; store SHA1 hashes 

712 with an empty salt. 

713 

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 """ 

718 

719 algorithm = "unsalted_sha1" 

720 

721 def salt(self): 

722 return "" 

723 

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 

729 

730 def decode(self, encoded): 

731 assert encoded.startswith("sha1$$") 

732 return { 

733 "algorithm": self.algorithm, 

734 "hash": encoded[6:], 

735 "salt": None, 

736 } 

737 

738 def verify(self, password, encoded): 

739 encoded_2 = self.encode(password, "") 

740 return constant_time_compare(encoded, encoded_2) 

741 

742 def safe_summary(self, encoded): 

743 decoded = self.decode(encoded) 

744 return { 

745 _("algorithm"): decoded["algorithm"], 

746 _("hash"): mask_hash(decoded["hash"]), 

747 } 

748 

749 def harden_runtime(self, password, encoded): 

750 pass 

751 

752 

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. 

758 

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 """ 

764 

765 algorithm = "unsalted_md5" 

766 

767 def salt(self): 

768 return "" 

769 

770 def encode(self, password, salt): 

771 if salt != "": 

772 raise ValueError("salt must be empty.") 

773 return hashlib.md5(password.encode()).hexdigest() 

774 

775 def decode(self, encoded): 

776 return { 

777 "algorithm": self.algorithm, 

778 "hash": encoded, 

779 "salt": None, 

780 } 

781 

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) 

787 

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 } 

794 

795 def harden_runtime(self, password, encoded): 

796 pass 

797 

798 

799class CryptPasswordHasher(BasePasswordHasher): 

800 """ 

801 Password hashing using UNIX crypt (not recommended) 

802 

803 The crypt module is not supported on all platforms. 

804 """ 

805 

806 algorithm = "crypt" 

807 library = "crypt" 

808 

809 def salt(self): 

810 return get_random_string(2) 

811 

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) 

821 

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 } 

830 

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) 

836 

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 } 

844 

845 def harden_runtime(self, password, encoded): 

846 pass