1"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants""" 2#============================================================================= 3# imports 4#============================================================================= 5# core 6import re 7import logging; log = logging.getLogger(__name__) 8from warnings import warn 9# site 10# pkg 11from passlib.utils import safe_crypt, test_crypt, to_unicode 12from passlib.utils.binary import h64, h64big 13from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause 14from passlib.crypto.des import des_encrypt_int_block 15import passlib.utils.handlers as uh 16# local 17__all__ = [ 18 "des_crypt", 19 "bsdi_crypt", 20 "bigcrypt", 21 "crypt16", 22] 23 24#============================================================================= 25# pure-python backend for des_crypt family 26#============================================================================= 27_BNULL = b'\x00' 28 29def _crypt_secret_to_key(secret): 30 """convert secret to 64-bit DES key. 31 32 this only uses the first 8 bytes of the secret, 33 and discards the high 8th bit of each byte at that. 34 a null parity bit is inserted after every 7th bit of the output. 35 """ 36 # NOTE: this would set the parity bits correctly, 37 # but des_encrypt_int_block() would just ignore them... 38 ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8) 39 ## for i, c in enumerate(secret[:8])) 40 return sum((byte_elem_value(c) & 0x7f) << (57-i*8) 41 for i, c in enumerate(secret[:8])) 42 43def _raw_des_crypt(secret, salt): 44 """pure-python backed for des_crypt""" 45 assert len(salt) == 2 46 47 # NOTE: some OSes will accept non-HASH64 characters in the salt, 48 # but what value they assign these characters varies wildy, 49 # so just rejecting them outright. 50 # the same goes for single-character salts... 51 # some OSes duplicate the char, some insert a '.' char, 52 # and openbsd does (something) which creates an invalid hash. 53 salt_value = h64.decode_int12(salt) 54 55 # gotta do something - no official policy since this predates unicode 56 if isinstance(secret, unicode): 57 secret = secret.encode("utf-8") 58 assert isinstance(secret, bytes) 59 60 # forbidding NULL char because underlying crypt() rejects them too. 61 if _BNULL in secret: 62 raise uh.exc.NullPasswordError(des_crypt) 63 64 # convert first 8 bytes of secret string into an integer 65 key_value = _crypt_secret_to_key(secret) 66 67 # run data through des using input of 0 68 result = des_encrypt_int_block(key_value, 0, salt_value, 25) 69 70 # run h64 encode on result 71 return h64big.encode_int64(result) 72 73def _bsdi_secret_to_key(secret): 74 """convert secret to DES key used by bsdi_crypt""" 75 key_value = _crypt_secret_to_key(secret) 76 idx = 8 77 end = len(secret) 78 while idx < end: 79 next = idx + 8 80 tmp_value = _crypt_secret_to_key(secret[idx:next]) 81 key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value 82 idx = next 83 return key_value 84 85def _raw_bsdi_crypt(secret, rounds, salt): 86 """pure-python backend for bsdi_crypt""" 87 88 # decode salt 89 salt_value = h64.decode_int24(salt) 90 91 # gotta do something - no official policy since this predates unicode 92 if isinstance(secret, unicode): 93 secret = secret.encode("utf-8") 94 assert isinstance(secret, bytes) 95 96 # forbidding NULL char because underlying crypt() rejects them too. 97 if _BNULL in secret: 98 raise uh.exc.NullPasswordError(bsdi_crypt) 99 100 # convert secret string into an integer 101 key_value = _bsdi_secret_to_key(secret) 102 103 # run data through des using input of 0 104 result = des_encrypt_int_block(key_value, 0, salt_value, rounds) 105 106 # run h64 encode on result 107 return h64big.encode_int64(result) 108 109#============================================================================= 110# handlers 111#============================================================================= 112class des_crypt(uh.TruncateMixin, uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): 113 """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`. 114 115 It supports a fixed-length salt. 116 117 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: 118 119 :type salt: str 120 :param salt: 121 Optional salt string. 122 If not specified, one will be autogenerated (this is recommended). 123 If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. 124 125 :param bool truncate_error: 126 By default, des_crypt will silently truncate passwords larger than 8 bytes. 127 Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` 128 to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. 129 130 .. versionadded:: 1.7 131 132 :type relaxed: bool 133 :param relaxed: 134 By default, providing an invalid value for one of the other 135 keywords will result in a :exc:`ValueError`. If ``relaxed=True``, 136 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` 137 will be issued instead. Correctable errors include 138 ``salt`` strings that are too long. 139 140 .. versionadded:: 1.6 141 """ 142 #=================================================================== 143 # class attrs 144 #=================================================================== 145 146 #-------------------- 147 # PasswordHash 148 #-------------------- 149 name = "des_crypt" 150 setting_kwds = ("salt", "truncate_error") 151 152 #-------------------- 153 # GenericHandler 154 #-------------------- 155 checksum_chars = uh.HASH64_CHARS 156 checksum_size = 11 157 158 #-------------------- 159 # HasSalt 160 #-------------------- 161 min_salt_size = max_salt_size = 2 162 salt_chars = uh.HASH64_CHARS 163 164 #-------------------- 165 # TruncateMixin 166 #-------------------- 167 truncate_size = 8 168 169 #=================================================================== 170 # formatting 171 #=================================================================== 172 # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum 173 174 _hash_regex = re.compile(u(r""" 175 ^ 176 (?P<salt>[./a-z0-9]{2}) 177 (?P<chk>[./a-z0-9]{11})? 178 $"""), re.X|re.I) 179 180 @classmethod 181 def from_string(cls, hash): 182 hash = to_unicode(hash, "ascii", "hash") 183 salt, chk = hash[:2], hash[2:] 184 return cls(salt=salt, checksum=chk or None) 185 186 def to_string(self): 187 hash = u("%s%s") % (self.salt, self.checksum) 188 return uascii_to_str(hash) 189 190 #=================================================================== 191 # digest calculation 192 #=================================================================== 193 def _calc_checksum(self, secret): 194 # check for truncation (during .hash() calls only) 195 if self.use_defaults: 196 self._check_truncate_policy(secret) 197 198 return self._calc_checksum_backend(secret) 199 200 #=================================================================== 201 # backend 202 #=================================================================== 203 backends = ("os_crypt", "builtin") 204 205 #--------------------------------------------------------------- 206 # os_crypt backend 207 #--------------------------------------------------------------- 208 @classmethod 209 def _load_backend_os_crypt(cls): 210 if test_crypt("test", 'abgOeLfPimXQo'): 211 cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) 212 return True 213 else: 214 return False 215 216 def _calc_checksum_os_crypt(self, secret): 217 # NOTE: we let safe_crypt() encode unicode secret -> utf8; 218 # no official policy since des-crypt predates unicode 219 hash = safe_crypt(secret, self.salt) 220 if hash: 221 assert hash.startswith(self.salt) and len(hash) == 13 222 return hash[2:] 223 else: 224 # py3's crypt.crypt() can't handle non-utf8 bytes. 225 # fallback to builtin alg, which is always available. 226 return self._calc_checksum_builtin(secret) 227 228 #--------------------------------------------------------------- 229 # builtin backend 230 #--------------------------------------------------------------- 231 @classmethod 232 def _load_backend_builtin(cls): 233 cls._set_calc_checksum_backend(cls._calc_checksum_builtin) 234 return True 235 236 def _calc_checksum_builtin(self, secret): 237 return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii") 238 239 #=================================================================== 240 # eoc 241 #=================================================================== 242 243class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): 244 """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`. 245 246 It supports a fixed-length salt, and a variable number of rounds. 247 248 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: 249 250 :type salt: str 251 :param salt: 252 Optional salt string. 253 If not specified, one will be autogenerated (this is recommended). 254 If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``. 255 256 :type rounds: int 257 :param rounds: 258 Optional number of rounds to use. 259 Defaults to 5001, must be between 1 and 16777215, inclusive. 260 261 :type relaxed: bool 262 :param relaxed: 263 By default, providing an invalid value for one of the other 264 keywords will result in a :exc:`ValueError`. If ``relaxed=True``, 265 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` 266 will be issued instead. Correctable errors include ``rounds`` 267 that are too small or too large, and ``salt`` strings that are too long. 268 269 .. versionadded:: 1.6 270 271 .. versionchanged:: 1.6 272 :meth:`hash` will now issue a warning if an even number of rounds is used 273 (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys). 274 """ 275 #=================================================================== 276 # class attrs 277 #=================================================================== 278 #--GenericHandler-- 279 name = "bsdi_crypt" 280 setting_kwds = ("salt", "rounds") 281 checksum_size = 11 282 checksum_chars = uh.HASH64_CHARS 283 284 #--HasSalt-- 285 min_salt_size = max_salt_size = 4 286 salt_chars = uh.HASH64_CHARS 287 288 #--HasRounds-- 289 default_rounds = 5001 290 min_rounds = 1 291 max_rounds = 16777215 # (1<<24)-1 292 rounds_cost = "linear" 293 294 # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds, 295 # but that seems to be an OS policy, not a algorithm limitation. 296 297 #=================================================================== 298 # parsing 299 #=================================================================== 300 _hash_regex = re.compile(u(r""" 301 ^ 302 _ 303 (?P<rounds>[./a-z0-9]{4}) 304 (?P<salt>[./a-z0-9]{4}) 305 (?P<chk>[./a-z0-9]{11})? 306 $"""), re.X|re.I) 307 308 @classmethod 309 def from_string(cls, hash): 310 hash = to_unicode(hash, "ascii", "hash") 311 m = cls._hash_regex.match(hash) 312 if not m: 313 raise uh.exc.InvalidHashError(cls) 314 rounds, salt, chk = m.group("rounds", "salt", "chk") 315 return cls( 316 rounds=h64.decode_int24(rounds.encode("ascii")), 317 salt=salt, 318 checksum=chk, 319 ) 320 321 def to_string(self): 322 hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"), 323 self.salt, self.checksum) 324 return uascii_to_str(hash) 325 326 #=================================================================== 327 # validation 328 #=================================================================== 329 330 # NOTE: keeping this flag for admin/choose_rounds.py script. 331 # want to eventually expose rounds logic to that script in better way. 332 _avoid_even_rounds = True 333 334 @classmethod 335 def using(cls, **kwds): 336 subcls = super(bsdi_crypt, cls).using(**kwds) 337 if not subcls.default_rounds & 1: 338 # issue warning if caller set an even 'rounds' value. 339 warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys", 340 uh.exc.PasslibSecurityWarning) 341 return subcls 342 343 @classmethod 344 def _generate_rounds(cls): 345 rounds = super(bsdi_crypt, cls)._generate_rounds() 346 # ensure autogenerated rounds are always odd 347 # NOTE: doing this even for default_rounds so needs_update() doesn't get 348 # caught in a loop. 349 # FIXME: this technically might generate a rounds value 1 larger 350 # than the requested upper bound - but better to err on side of safety. 351 return rounds|1 352 353 #=================================================================== 354 # migration 355 #=================================================================== 356 357 def _calc_needs_update(self, **kwds): 358 # mark bsdi_crypt hashes as deprecated if they have even rounds. 359 if not self.rounds & 1: 360 return True 361 # hand off to base implementation 362 return super(bsdi_crypt, self)._calc_needs_update(**kwds) 363 364 #=================================================================== 365 # backends 366 #=================================================================== 367 backends = ("os_crypt", "builtin") 368 369 #--------------------------------------------------------------- 370 # os_crypt backend 371 #--------------------------------------------------------------- 372 @classmethod 373 def _load_backend_os_crypt(cls): 374 if test_crypt("test", '_/...lLDAxARksGCHin.'): 375 cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) 376 return True 377 else: 378 return False 379 380 def _calc_checksum_os_crypt(self, secret): 381 config = self.to_string() 382 hash = safe_crypt(secret, config) 383 if hash: 384 assert hash.startswith(config[:9]) and len(hash) == 20 385 return hash[-11:] 386 else: 387 # py3's crypt.crypt() can't handle non-utf8 bytes. 388 # fallback to builtin alg, which is always available. 389 return self._calc_checksum_builtin(secret) 390 391 #--------------------------------------------------------------- 392 # builtin backend 393 #--------------------------------------------------------------- 394 @classmethod 395 def _load_backend_builtin(cls): 396 cls._set_calc_checksum_backend(cls._calc_checksum_builtin) 397 return True 398 399 def _calc_checksum_builtin(self, secret): 400 return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") 401 402 #=================================================================== 403 # eoc 404 #=================================================================== 405 406class bigcrypt(uh.HasSalt, uh.GenericHandler): 407 """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`. 408 409 It supports a fixed-length salt. 410 411 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: 412 413 :type salt: str 414 :param salt: 415 Optional salt string. 416 If not specified, one will be autogenerated (this is recommended). 417 If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. 418 419 :type relaxed: bool 420 :param relaxed: 421 By default, providing an invalid value for one of the other 422 keywords will result in a :exc:`ValueError`. If ``relaxed=True``, 423 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` 424 will be issued instead. Correctable errors include 425 ``salt`` strings that are too long. 426 427 .. versionadded:: 1.6 428 """ 429 #=================================================================== 430 # class attrs 431 #=================================================================== 432 #--GenericHandler-- 433 name = "bigcrypt" 434 setting_kwds = ("salt",) 435 checksum_chars = uh.HASH64_CHARS 436 # NOTE: checksum chars must be multiple of 11 437 438 #--HasSalt-- 439 min_salt_size = max_salt_size = 2 440 salt_chars = uh.HASH64_CHARS 441 442 #=================================================================== 443 # internal helpers 444 #=================================================================== 445 _hash_regex = re.compile(u(r""" 446 ^ 447 (?P<salt>[./a-z0-9]{2}) 448 (?P<chk>([./a-z0-9]{11})+)? 449 $"""), re.X|re.I) 450 451 @classmethod 452 def from_string(cls, hash): 453 hash = to_unicode(hash, "ascii", "hash") 454 m = cls._hash_regex.match(hash) 455 if not m: 456 raise uh.exc.InvalidHashError(cls) 457 salt, chk = m.group("salt", "chk") 458 return cls(salt=salt, checksum=chk) 459 460 def to_string(self): 461 hash = u("%s%s") % (self.salt, self.checksum) 462 return uascii_to_str(hash) 463 464 def _norm_checksum(self, checksum, relaxed=False): 465 checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed) 466 if len(checksum) % 11: 467 raise uh.exc.InvalidHashError(self) 468 return checksum 469 470 #=================================================================== 471 # backend 472 #=================================================================== 473 def _calc_checksum(self, secret): 474 if isinstance(secret, unicode): 475 secret = secret.encode("utf-8") 476 chk = _raw_des_crypt(secret, self.salt.encode("ascii")) 477 idx = 8 478 end = len(secret) 479 while idx < end: 480 next = idx + 8 481 chk += _raw_des_crypt(secret[idx:next], chk[-11:-9]) 482 idx = next 483 return chk.decode("ascii") 484 485 #=================================================================== 486 # eoc 487 #=================================================================== 488 489class crypt16(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler): 490 """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`. 491 492 It supports a fixed-length salt. 493 494 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: 495 496 :type salt: str 497 :param salt: 498 Optional salt string. 499 If not specified, one will be autogenerated (this is recommended). 500 If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. 501 502 :param bool truncate_error: 503 By default, crypt16 will silently truncate passwords larger than 16 bytes. 504 Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` 505 to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. 506 507 .. versionadded:: 1.7 508 509 :type relaxed: bool 510 :param relaxed: 511 By default, providing an invalid value for one of the other 512 keywords will result in a :exc:`ValueError`. If ``relaxed=True``, 513 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` 514 will be issued instead. Correctable errors include 515 ``salt`` strings that are too long. 516 517 .. versionadded:: 1.6 518 """ 519 #=================================================================== 520 # class attrs 521 #=================================================================== 522 523 #-------------------- 524 # PasswordHash 525 #-------------------- 526 name = "crypt16" 527 setting_kwds = ("salt", "truncate_error") 528 529 #-------------------- 530 # GenericHandler 531 #-------------------- 532 checksum_size = 22 533 checksum_chars = uh.HASH64_CHARS 534 535 #-------------------- 536 # HasSalt 537 #-------------------- 538 min_salt_size = max_salt_size = 2 539 salt_chars = uh.HASH64_CHARS 540 541 #-------------------- 542 # TruncateMixin 543 #-------------------- 544 truncate_size = 16 545 546 #=================================================================== 547 # internal helpers 548 #=================================================================== 549 _hash_regex = re.compile(u(r""" 550 ^ 551 (?P<salt>[./a-z0-9]{2}) 552 (?P<chk>[./a-z0-9]{22})? 553 $"""), re.X|re.I) 554 555 @classmethod 556 def from_string(cls, hash): 557 hash = to_unicode(hash, "ascii", "hash") 558 m = cls._hash_regex.match(hash) 559 if not m: 560 raise uh.exc.InvalidHashError(cls) 561 salt, chk = m.group("salt", "chk") 562 return cls(salt=salt, checksum=chk) 563 564 def to_string(self): 565 hash = u("%s%s") % (self.salt, self.checksum) 566 return uascii_to_str(hash) 567 568 #=================================================================== 569 # backend 570 #=================================================================== 571 def _calc_checksum(self, secret): 572 if isinstance(secret, unicode): 573 secret = secret.encode("utf-8") 574 575 # check for truncation (during .hash() calls only) 576 if self.use_defaults: 577 self._check_truncate_policy(secret) 578 579 # parse salt value 580 try: 581 salt_value = h64.decode_int12(self.salt.encode("ascii")) 582 except ValueError: # pragma: no cover - caught by class 583 raise suppress_cause(ValueError("invalid chars in salt")) 584 585 # convert first 8 byts of secret string into an integer, 586 key1 = _crypt_secret_to_key(secret) 587 588 # run data through des using input of 0 589 result1 = des_encrypt_int_block(key1, 0, salt_value, 20) 590 591 # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars) 592 key2 = _crypt_secret_to_key(secret[8:16]) 593 594 # run data through des using input of 0 595 result2 = des_encrypt_int_block(key2, 0, salt_value, 5) 596 597 # done 598 chk = h64big.encode_int64(result1) + h64big.encode_int64(result2) 599 return chk.decode("ascii") 600 601 #=================================================================== 602 # eoc 603 #=================================================================== 604 605#============================================================================= 606# eof 607#============================================================================= 608