123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- """passlib.handlers.des_crypt - traditional unix (DES) crypt and variants"""
- #=============================================================================
- # imports
- #=============================================================================
- # core
- import re
- import logging; log = logging.getLogger(__name__)
- from warnings import warn
- # site
- # pkg
- from passlib.utils import safe_crypt, test_crypt, to_unicode
- from passlib.utils.binary import h64, h64big
- from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause
- from passlib.crypto.des import des_encrypt_int_block
- import passlib.utils.handlers as uh
- # local
- __all__ = [
- "des_crypt",
- "bsdi_crypt",
- "bigcrypt",
- "crypt16",
- ]
- #=============================================================================
- # pure-python backend for des_crypt family
- #=============================================================================
- _BNULL = b'\x00'
- def _crypt_secret_to_key(secret):
- """convert secret to 64-bit DES key.
- this only uses the first 8 bytes of the secret,
- and discards the high 8th bit of each byte at that.
- a null parity bit is inserted after every 7th bit of the output.
- """
- # NOTE: this would set the parity bits correctly,
- # but des_encrypt_int_block() would just ignore them...
- ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8)
- ## for i, c in enumerate(secret[:8]))
- return sum((byte_elem_value(c) & 0x7f) << (57-i*8)
- for i, c in enumerate(secret[:8]))
- def _raw_des_crypt(secret, salt):
- """pure-python backed for des_crypt"""
- assert len(salt) == 2
- # NOTE: some OSes will accept non-HASH64 characters in the salt,
- # but what value they assign these characters varies wildy,
- # so just rejecting them outright.
- # the same goes for single-character salts...
- # some OSes duplicate the char, some insert a '.' char,
- # and openbsd does (something) which creates an invalid hash.
- salt_value = h64.decode_int12(salt)
- # gotta do something - no official policy since this predates unicode
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- assert isinstance(secret, bytes)
- # forbidding NULL char because underlying crypt() rejects them too.
- if _BNULL in secret:
- raise uh.exc.NullPasswordError(des_crypt)
- # convert first 8 bytes of secret string into an integer
- key_value = _crypt_secret_to_key(secret)
- # run data through des using input of 0
- result = des_encrypt_int_block(key_value, 0, salt_value, 25)
- # run h64 encode on result
- return h64big.encode_int64(result)
- def _bsdi_secret_to_key(secret):
- """convert secret to DES key used by bsdi_crypt"""
- key_value = _crypt_secret_to_key(secret)
- idx = 8
- end = len(secret)
- while idx < end:
- next = idx + 8
- tmp_value = _crypt_secret_to_key(secret[idx:next])
- key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value
- idx = next
- return key_value
- def _raw_bsdi_crypt(secret, rounds, salt):
- """pure-python backend for bsdi_crypt"""
- # decode salt
- salt_value = h64.decode_int24(salt)
- # gotta do something - no official policy since this predates unicode
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- assert isinstance(secret, bytes)
- # forbidding NULL char because underlying crypt() rejects them too.
- if _BNULL in secret:
- raise uh.exc.NullPasswordError(bsdi_crypt)
- # convert secret string into an integer
- key_value = _bsdi_secret_to_key(secret)
- # run data through des using input of 0
- result = des_encrypt_int_block(key_value, 0, salt_value, rounds)
- # run h64 encode on result
- return h64big.encode_int64(result)
- #=============================================================================
- # handlers
- #=============================================================================
- class des_crypt(uh.TruncateMixin, uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
- """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`.
- It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
- :type salt: str
- :param salt:
- Optional salt string.
- If not specified, one will be autogenerated (this is recommended).
- If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
- :param bool truncate_error:
- By default, des_crypt will silently truncate passwords larger than 8 bytes.
- Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
- to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
- .. versionadded:: 1.7
- :type relaxed: bool
- :param relaxed:
- By default, providing an invalid value for one of the other
- keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
- and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
- will be issued instead. Correctable errors include
- ``salt`` strings that are too long.
- .. versionadded:: 1.6
- """
- #===================================================================
- # class attrs
- #===================================================================
- #--------------------
- # PasswordHash
- #--------------------
- name = "des_crypt"
- setting_kwds = ("salt", "truncate_error")
- #--------------------
- # GenericHandler
- #--------------------
- checksum_chars = uh.HASH64_CHARS
- checksum_size = 11
- #--------------------
- # HasSalt
- #--------------------
- min_salt_size = max_salt_size = 2
- salt_chars = uh.HASH64_CHARS
- #--------------------
- # TruncateMixin
- #--------------------
- truncate_size = 8
- #===================================================================
- # formatting
- #===================================================================
- # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
- _hash_regex = re.compile(u(r"""
- ^
- (?P<salt>[./a-z0-9]{2})
- (?P<chk>[./a-z0-9]{11})?
- $"""), re.X|re.I)
- @classmethod
- def from_string(cls, hash):
- hash = to_unicode(hash, "ascii", "hash")
- salt, chk = hash[:2], hash[2:]
- return cls(salt=salt, checksum=chk or None)
- def to_string(self):
- hash = u("%s%s") % (self.salt, self.checksum)
- return uascii_to_str(hash)
- #===================================================================
- # digest calculation
- #===================================================================
- def _calc_checksum(self, secret):
- # check for truncation (during .hash() calls only)
- if self.use_defaults:
- self._check_truncate_policy(secret)
- return self._calc_checksum_backend(secret)
- #===================================================================
- # backend
- #===================================================================
- backends = ("os_crypt", "builtin")
- #---------------------------------------------------------------
- # os_crypt backend
- #---------------------------------------------------------------
- @classmethod
- def _load_backend_os_crypt(cls):
- if test_crypt("test", 'abgOeLfPimXQo'):
- cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
- return True
- else:
- return False
- def _calc_checksum_os_crypt(self, secret):
- # NOTE: we let safe_crypt() encode unicode secret -> utf8;
- # no official policy since des-crypt predates unicode
- hash = safe_crypt(secret, self.salt)
- if hash:
- assert hash.startswith(self.salt) and len(hash) == 13
- return hash[2:]
- else:
- # py3's crypt.crypt() can't handle non-utf8 bytes.
- # fallback to builtin alg, which is always available.
- return self._calc_checksum_builtin(secret)
- #---------------------------------------------------------------
- # builtin backend
- #---------------------------------------------------------------
- @classmethod
- def _load_backend_builtin(cls):
- cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
- return True
- def _calc_checksum_builtin(self, secret):
- return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")
- #===================================================================
- # eoc
- #===================================================================
- class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
- """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`.
- It supports a fixed-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
- :type salt: str
- :param salt:
- Optional salt string.
- If not specified, one will be autogenerated (this is recommended).
- If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
- :type rounds: int
- :param rounds:
- Optional number of rounds to use.
- Defaults to 5001, must be between 1 and 16777215, inclusive.
- :type relaxed: bool
- :param relaxed:
- By default, providing an invalid value for one of the other
- keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
- and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
- will be issued instead. Correctable errors include ``rounds``
- that are too small or too large, and ``salt`` strings that are too long.
- .. versionadded:: 1.6
- .. versionchanged:: 1.6
- :meth:`hash` will now issue a warning if an even number of rounds is used
- (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys).
- """
- #===================================================================
- # class attrs
- #===================================================================
- #--GenericHandler--
- name = "bsdi_crypt"
- setting_kwds = ("salt", "rounds")
- checksum_size = 11
- checksum_chars = uh.HASH64_CHARS
- #--HasSalt--
- min_salt_size = max_salt_size = 4
- salt_chars = uh.HASH64_CHARS
- #--HasRounds--
- default_rounds = 5001
- min_rounds = 1
- max_rounds = 16777215 # (1<<24)-1
- rounds_cost = "linear"
- # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds,
- # but that seems to be an OS policy, not a algorithm limitation.
- #===================================================================
- # parsing
- #===================================================================
- _hash_regex = re.compile(u(r"""
- ^
- _
- (?P<rounds>[./a-z0-9]{4})
- (?P<salt>[./a-z0-9]{4})
- (?P<chk>[./a-z0-9]{11})?
- $"""), re.X|re.I)
- @classmethod
- def from_string(cls, hash):
- hash = to_unicode(hash, "ascii", "hash")
- m = cls._hash_regex.match(hash)
- if not m:
- raise uh.exc.InvalidHashError(cls)
- rounds, salt, chk = m.group("rounds", "salt", "chk")
- return cls(
- rounds=h64.decode_int24(rounds.encode("ascii")),
- salt=salt,
- checksum=chk,
- )
- def to_string(self):
- hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"),
- self.salt, self.checksum)
- return uascii_to_str(hash)
- #===================================================================
- # validation
- #===================================================================
- # NOTE: keeping this flag for admin/choose_rounds.py script.
- # want to eventually expose rounds logic to that script in better way.
- _avoid_even_rounds = True
- @classmethod
- def using(cls, **kwds):
- subcls = super(bsdi_crypt, cls).using(**kwds)
- if not subcls.default_rounds & 1:
- # issue warning if caller set an even 'rounds' value.
- warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys",
- uh.exc.PasslibSecurityWarning)
- return subcls
- @classmethod
- def _generate_rounds(cls):
- rounds = super(bsdi_crypt, cls)._generate_rounds()
- # ensure autogenerated rounds are always odd
- # NOTE: doing this even for default_rounds so needs_update() doesn't get
- # caught in a loop.
- # FIXME: this technically might generate a rounds value 1 larger
- # than the requested upper bound - but better to err on side of safety.
- return rounds|1
- #===================================================================
- # migration
- #===================================================================
- def _calc_needs_update(self, **kwds):
- # mark bsdi_crypt hashes as deprecated if they have even rounds.
- if not self.rounds & 1:
- return True
- # hand off to base implementation
- return super(bsdi_crypt, self)._calc_needs_update(**kwds)
- #===================================================================
- # backends
- #===================================================================
- backends = ("os_crypt", "builtin")
- #---------------------------------------------------------------
- # os_crypt backend
- #---------------------------------------------------------------
- @classmethod
- def _load_backend_os_crypt(cls):
- if test_crypt("test", '_/...lLDAxARksGCHin.'):
- cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
- return True
- else:
- return False
- def _calc_checksum_os_crypt(self, secret):
- config = self.to_string()
- hash = safe_crypt(secret, config)
- if hash:
- assert hash.startswith(config[:9]) and len(hash) == 20
- return hash[-11:]
- else:
- # py3's crypt.crypt() can't handle non-utf8 bytes.
- # fallback to builtin alg, which is always available.
- return self._calc_checksum_builtin(secret)
- #---------------------------------------------------------------
- # builtin backend
- #---------------------------------------------------------------
- @classmethod
- def _load_backend_builtin(cls):
- cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
- return True
- def _calc_checksum_builtin(self, secret):
- return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
- #===================================================================
- # eoc
- #===================================================================
- class bigcrypt(uh.HasSalt, uh.GenericHandler):
- """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`.
- It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
- :type salt: str
- :param salt:
- Optional salt string.
- If not specified, one will be autogenerated (this is recommended).
- If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
- :type relaxed: bool
- :param relaxed:
- By default, providing an invalid value for one of the other
- keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
- and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
- will be issued instead. Correctable errors include
- ``salt`` strings that are too long.
- .. versionadded:: 1.6
- """
- #===================================================================
- # class attrs
- #===================================================================
- #--GenericHandler--
- name = "bigcrypt"
- setting_kwds = ("salt",)
- checksum_chars = uh.HASH64_CHARS
- # NOTE: checksum chars must be multiple of 11
- #--HasSalt--
- min_salt_size = max_salt_size = 2
- salt_chars = uh.HASH64_CHARS
- #===================================================================
- # internal helpers
- #===================================================================
- _hash_regex = re.compile(u(r"""
- ^
- (?P<salt>[./a-z0-9]{2})
- (?P<chk>([./a-z0-9]{11})+)?
- $"""), re.X|re.I)
- @classmethod
- def from_string(cls, hash):
- hash = to_unicode(hash, "ascii", "hash")
- m = cls._hash_regex.match(hash)
- if not m:
- raise uh.exc.InvalidHashError(cls)
- salt, chk = m.group("salt", "chk")
- return cls(salt=salt, checksum=chk)
- def to_string(self):
- hash = u("%s%s") % (self.salt, self.checksum)
- return uascii_to_str(hash)
- def _norm_checksum(self, checksum, relaxed=False):
- checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed)
- if len(checksum) % 11:
- raise uh.exc.InvalidHashError(self)
- return checksum
- #===================================================================
- # backend
- #===================================================================
- def _calc_checksum(self, secret):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- chk = _raw_des_crypt(secret, self.salt.encode("ascii"))
- idx = 8
- end = len(secret)
- while idx < end:
- next = idx + 8
- chk += _raw_des_crypt(secret[idx:next], chk[-11:-9])
- idx = next
- return chk.decode("ascii")
- #===================================================================
- # eoc
- #===================================================================
- class crypt16(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler):
- """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`.
- It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
- :type salt: str
- :param salt:
- Optional salt string.
- If not specified, one will be autogenerated (this is recommended).
- If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
- :param bool truncate_error:
- By default, crypt16 will silently truncate passwords larger than 16 bytes.
- Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
- to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
- .. versionadded:: 1.7
- :type relaxed: bool
- :param relaxed:
- By default, providing an invalid value for one of the other
- keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
- and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
- will be issued instead. Correctable errors include
- ``salt`` strings that are too long.
- .. versionadded:: 1.6
- """
- #===================================================================
- # class attrs
- #===================================================================
- #--------------------
- # PasswordHash
- #--------------------
- name = "crypt16"
- setting_kwds = ("salt", "truncate_error")
- #--------------------
- # GenericHandler
- #--------------------
- checksum_size = 22
- checksum_chars = uh.HASH64_CHARS
- #--------------------
- # HasSalt
- #--------------------
- min_salt_size = max_salt_size = 2
- salt_chars = uh.HASH64_CHARS
- #--------------------
- # TruncateMixin
- #--------------------
- truncate_size = 16
- #===================================================================
- # internal helpers
- #===================================================================
- _hash_regex = re.compile(u(r"""
- ^
- (?P<salt>[./a-z0-9]{2})
- (?P<chk>[./a-z0-9]{22})?
- $"""), re.X|re.I)
- @classmethod
- def from_string(cls, hash):
- hash = to_unicode(hash, "ascii", "hash")
- m = cls._hash_regex.match(hash)
- if not m:
- raise uh.exc.InvalidHashError(cls)
- salt, chk = m.group("salt", "chk")
- return cls(salt=salt, checksum=chk)
- def to_string(self):
- hash = u("%s%s") % (self.salt, self.checksum)
- return uascii_to_str(hash)
- #===================================================================
- # backend
- #===================================================================
- def _calc_checksum(self, secret):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- # check for truncation (during .hash() calls only)
- if self.use_defaults:
- self._check_truncate_policy(secret)
- # parse salt value
- try:
- salt_value = h64.decode_int12(self.salt.encode("ascii"))
- except ValueError: # pragma: no cover - caught by class
- raise suppress_cause(ValueError("invalid chars in salt"))
- # convert first 8 byts of secret string into an integer,
- key1 = _crypt_secret_to_key(secret)
- # run data through des using input of 0
- result1 = des_encrypt_int_block(key1, 0, salt_value, 20)
- # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars)
- key2 = _crypt_secret_to_key(secret[8:16])
- # run data through des using input of 0
- result2 = des_encrypt_int_block(key2, 0, salt_value, 5)
- # done
- chk = h64big.encode_int64(result1) + h64big.encode_int64(result2)
- return chk.decode("ascii")
- #===================================================================
- # eoc
- #===================================================================
- #=============================================================================
- # eof
- #=============================================================================
|