codekingpro/portable-devtools
114k
1"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants"""2#=============================================================================3# imports4#=============================================================================5# core6import re7import logging; log = logging.getLogger(__name__)8from warnings import warn9# site10# pkg11from passlib.utils import safe_crypt, test_crypt, to_unicode12from passlib.utils.binary import h64, h64big13from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause14from passlib.crypto.des import des_encrypt_int_block15import passlib.utils.handlers as uh16# local17__all__ = [18 "des_crypt",19 "bsdi_crypt",20 "bigcrypt",21 "crypt16",22]23 24#=============================================================================25# pure-python backend for des_crypt family26#=============================================================================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) == 246 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 unicode56 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 integer65 key_value = _crypt_secret_to_key(secret)66 67 # run data through des using input of 068 result = des_encrypt_int_block(key_value, 0, salt_value, 25)69 70 # run h64 encode on result71 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 = 877 end = len(secret)78 while idx < end:79 next = idx + 880 tmp_value = _crypt_secret_to_key(secret[idx:next])81 key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value82 idx = next83 return key_value84 85def _raw_bsdi_crypt(secret, rounds, salt):86 """pure-python backend for bsdi_crypt"""87 88 # decode salt89 salt_value = h64.decode_int24(salt)90 91 # gotta do something - no official policy since this predates unicode92 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 integer101 key_value = _bsdi_secret_to_key(secret)102 103 # run data through des using input of 0104 result = des_encrypt_int_block(key_value, 0, salt_value, rounds)105 106 # run h64 encode on result107 return h64big.encode_int64(result)108 109#=============================================================================110# handlers111#=============================================================================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: str120 :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.7131 132 :type relaxed: bool133 :param relaxed:134 By default, providing an invalid value for one of the other135 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 include138 ``salt`` strings that are too long.139 140 .. versionadded:: 1.6141 """142 #===================================================================143 # class attrs144 #===================================================================145 146 #--------------------147 # PasswordHash148 #--------------------149 name = "des_crypt"150 setting_kwds = ("salt", "truncate_error")151 152 #--------------------153 # GenericHandler154 #--------------------155 checksum_chars = uh.HASH64_CHARS156 checksum_size = 11157 158 #--------------------159 # HasSalt160 #--------------------161 min_salt_size = max_salt_size = 2162 salt_chars = uh.HASH64_CHARS163 164 #--------------------165 # TruncateMixin166 #--------------------167 truncate_size = 8168 169 #===================================================================170 # formatting171 #===================================================================172 # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum173 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 @classmethod181 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 calculation192 #===================================================================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 # backend202 #===================================================================203 backends = ("os_crypt", "builtin")204 205 #---------------------------------------------------------------206 # os_crypt backend207 #---------------------------------------------------------------208 @classmethod209 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 True213 else:214 return False215 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 unicode219 hash = safe_crypt(secret, self.salt)220 if hash is None:221 # py3's crypt.crypt() can't handle non-utf8 bytes.222 # fallback to builtin alg, which is always available.223 return self._calc_checksum_builtin(secret)224 if not hash.startswith(self.salt) or len(hash) != 13:225 raise uh.exc.CryptBackendError(self, self.salt, hash)226 return hash[2:]227 228 #---------------------------------------------------------------229 # builtin backend230 #---------------------------------------------------------------231 @classmethod232 def _load_backend_builtin(cls):233 cls._set_calc_checksum_backend(cls._calc_checksum_builtin)234 return True235 236 def _calc_checksum_builtin(self, secret):237 return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")238 239 #===================================================================240 # eoc241 #===================================================================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: str251 :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: int257 :param rounds:258 Optional number of rounds to use.259 Defaults to 5001, must be between 1 and 16777215, inclusive.260 261 :type relaxed: bool262 :param relaxed:263 By default, providing an invalid value for one of the other264 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.6270 271 .. versionchanged:: 1.6272 :meth:`hash` will now issue a warning if an even number of rounds is used273 (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys).274 """275 #===================================================================276 # class attrs277 #===================================================================278 #--GenericHandler--279 name = "bsdi_crypt"280 setting_kwds = ("salt", "rounds")281 checksum_size = 11282 checksum_chars = uh.HASH64_CHARS283 284 #--HasSalt--285 min_salt_size = max_salt_size = 4286 salt_chars = uh.HASH64_CHARS287 288 #--HasRounds--289 default_rounds = 5001290 min_rounds = 1291 max_rounds = 16777215 # (1<<24)-1292 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 # parsing299 #===================================================================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 @classmethod309 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 # validation328 #===================================================================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 = True333 334 @classmethod335 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 subcls342 343 @classmethod344 def _generate_rounds(cls):345 rounds = super(bsdi_crypt, cls)._generate_rounds()346 # ensure autogenerated rounds are always odd347 # NOTE: doing this even for default_rounds so needs_update() doesn't get348 # caught in a loop.349 # FIXME: this technically might generate a rounds value 1 larger350 # than the requested upper bound - but better to err on side of safety.351 return rounds|1352 353 #===================================================================354 # migration355 #===================================================================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 True361 # hand off to base implementation362 return super(bsdi_crypt, self)._calc_needs_update(**kwds)363 364 #===================================================================365 # backends366 #===================================================================367 backends = ("os_crypt", "builtin")368 369 #---------------------------------------------------------------370 # os_crypt backend371 #---------------------------------------------------------------372 @classmethod373 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 True377 else:378 return False379 380 def _calc_checksum_os_crypt(self, secret):381 config = self.to_string()382 hash = safe_crypt(secret, config)383 if hash is None:384 # py3's crypt.crypt() can't handle non-utf8 bytes.385 # fallback to builtin alg, which is always available.386 return self._calc_checksum_builtin(secret)387 if not hash.startswith(config[:9]) or len(hash) != 20:388 raise uh.exc.CryptBackendError(self, config, hash)389 return hash[-11:]390 391 #---------------------------------------------------------------392 # builtin backend393 #---------------------------------------------------------------394 @classmethod395 def _load_backend_builtin(cls):396 cls._set_calc_checksum_backend(cls._calc_checksum_builtin)397 return True398 399 def _calc_checksum_builtin(self, secret):400 return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")401 402 #===================================================================403 # eoc404 #===================================================================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: str414 :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: bool420 :param relaxed:421 By default, providing an invalid value for one of the other422 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 include425 ``salt`` strings that are too long.426 427 .. versionadded:: 1.6428 """429 #===================================================================430 # class attrs431 #===================================================================432 #--GenericHandler--433 name = "bigcrypt"434 setting_kwds = ("salt",)435 checksum_chars = uh.HASH64_CHARS436 # NOTE: checksum chars must be multiple of 11437 438 #--HasSalt--439 min_salt_size = max_salt_size = 2440 salt_chars = uh.HASH64_CHARS441 442 #===================================================================443 # internal helpers444 #===================================================================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 @classmethod452 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 checksum469 470 #===================================================================471 # backend472 #===================================================================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 = 8478 end = len(secret)479 while idx < end:480 next = idx + 8481 chk += _raw_des_crypt(secret[idx:next], chk[-11:-9])482 idx = next483 return chk.decode("ascii")484 485 #===================================================================486 # eoc487 #===================================================================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: str497 :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.7508 509 :type relaxed: bool510 :param relaxed:511 By default, providing an invalid value for one of the other512 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 include515 ``salt`` strings that are too long.516 517 .. versionadded:: 1.6518 """519 #===================================================================520 # class attrs521 #===================================================================522 523 #--------------------524 # PasswordHash525 #--------------------526 name = "crypt16"527 setting_kwds = ("salt", "truncate_error")528 529 #--------------------530 # GenericHandler531 #--------------------532 checksum_size = 22533 checksum_chars = uh.HASH64_CHARS534 535 #--------------------536 # HasSalt537 #--------------------538 min_salt_size = max_salt_size = 2539 salt_chars = uh.HASH64_CHARS540 541 #--------------------542 # TruncateMixin543 #--------------------544 truncate_size = 16545 546 #===================================================================547 # internal helpers548 #===================================================================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 @classmethod556 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 # backend570 #===================================================================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 value580 try:581 salt_value = h64.decode_int12(self.salt.encode("ascii"))582 except ValueError: # pragma: no cover - caught by class583 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 0589 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 0595 result2 = des_encrypt_int_block(key2, 0, salt_value, 5)596 597 # done598 chk = h64big.encode_int64(result1) + h64big.encode_int64(result2)599 return chk.decode("ascii")600 601 #===================================================================602 # eoc603 #===================================================================604 605#=============================================================================606# eof607#=============================================================================608 