Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
sha2_crypt.py535 linesDownload Raw Back to handlers
1"""passlib.handlers.sha2_crypt - SHA256-Crypt / SHA512-Crypt"""2#=============================================================================3# imports4#=============================================================================5# core6import hashlib7import logging; log = logging.getLogger(__name__)8# site9# pkg10from passlib.utils import safe_crypt, test_crypt, \11                          repeat_string, to_unicode12from passlib.utils.binary import h6413from passlib.utils.compat import byte_elem_value, u, \14                                 uascii_to_str, unicode15import passlib.utils.handlers as uh16# local17__all__ = [18    "sha512_crypt",19    "sha256_crypt",20]21 22#=============================================================================23# pure-python backend, used by both sha256_crypt & sha512_crypt24# when crypt.crypt() backend is not available.25#=============================================================================26_BNULL = b'\x00'27 28# pre-calculated offsets used to speed up C digest stage (see notes below).29# sequence generated using the following:30    ##perms_order = "p,pp,ps,psp,sp,spp".split(",")31    ##def offset(i):32    ##    key = (("p" if i % 2 else "") + ("s" if i % 3 else "") +33    ##        ("p" if i % 7 else "") + ("" if i % 2 else "p"))34    ##    return perms_order.index(key)35    ##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)]36_c_digest_offsets = (37    (0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3),38    (4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1),39    (4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3),40    )41 42# map used to transpose bytes when encoding final sha256_crypt digest43_256_transpose_map = (44    20, 10,  0, 11,  1, 21,  2, 22, 12, 23, 13,  3, 14,  4, 24,  5,45    25, 15, 26, 16,  6, 17,  7, 27,  8, 28, 18, 29, 19,  9, 30, 31,46)47 48# map used to transpose bytes when encoding final sha512_crypt digest49_512_transpose_map = (50    42, 21,  0,  1, 43, 22, 23,  2, 44, 45, 24,  3,  4, 46, 25, 26,51     5, 47, 48, 27,  6,  7, 49, 28, 29,  8, 50, 51, 30,  9, 10, 52,52    31, 32, 11, 53, 54, 33, 12, 13, 55, 34, 35, 14, 56, 57, 36, 15,53    16, 58, 37, 38, 17, 59, 60, 39, 18, 19, 61, 40, 41, 20, 62, 63,54)55 56def _raw_sha2_crypt(pwd, salt, rounds, use_512=False):57    """perform raw sha256-crypt / sha512-crypt58 59    this function provides a pure-python implementation of the internals60    for the SHA256-Crypt and SHA512-Crypt algorithms; it doesn't61    handle any of the parsing/validation of the hash strings themselves.62 63    :arg pwd: password chars/bytes to hash64    :arg salt: salt chars to use65    :arg rounds: linear rounds cost66    :arg use_512: use sha512-crypt instead of sha256-crypt mode67 68    :returns:69        encoded checksum chars70    """71    #===================================================================72    # init & validate inputs73    #===================================================================74 75    # NOTE: the setup portion of this algorithm scales ~linearly in time76    #       with the size of the password, making it vulnerable to a DOS from77    #       unreasonably large inputs. the following code has some optimizations78    #       which would make things even worse, using O(pwd_len**2) memory79    #       when calculating digest P. 80    #81    #       to mitigate these two issues: 1) this code switches to a 82    #       O(pwd_len)-memory algorithm for passwords that are much larger 83    #       than average, and 2) Passlib enforces a library-wide max limit on84    #       the size of passwords it will allow, to prevent this algorithm and 85    #       others from being DOSed in this way (see passlib.exc.PasswordSizeError86    #       for details).87 88    # validate secret89    if isinstance(pwd, unicode):90        # XXX: not sure what official unicode policy is, using this as default91        pwd = pwd.encode("utf-8")92    assert isinstance(pwd, bytes)93    if _BNULL in pwd:94        raise uh.exc.NullPasswordError(sha512_crypt if use_512 else sha256_crypt)95    pwd_len = len(pwd)96 97    # validate rounds98    assert 1000 <= rounds <= 999999999, "invalid rounds"99        # NOTE: spec says out-of-range rounds should be clipped, instead of100        # causing an error. this function assumes that's been taken care of101        # by the handler class.102 103    # validate salt104    assert isinstance(salt, unicode), "salt not unicode"105    salt = salt.encode("ascii")106    salt_len = len(salt)107    assert salt_len < 17, "salt too large"108        # NOTE: spec says salts larger than 16 bytes should be truncated,109        # instead of causing an error. this function assumes that's been110        # taken care of by the handler class.111 112    # load sha256/512 specific constants113    if use_512:114        hash_const = hashlib.sha512115        transpose_map = _512_transpose_map116    else:117        hash_const = hashlib.sha256118        transpose_map = _256_transpose_map119 120    #===================================================================121    # digest B - used as subinput to digest A122    #===================================================================123    db = hash_const(pwd + salt + pwd).digest()124 125    #===================================================================126    # digest A - used to initialize first round of digest C127    #===================================================================128    # start out with pwd + salt129    a_ctx = hash_const(pwd + salt)130    a_ctx_update = a_ctx.update131 132    # add pwd_len bytes of b, repeating b as many times as needed.133    a_ctx_update(repeat_string(db, pwd_len))134 135    # for each bit in pwd_len: add b if it's 1, or pwd if it's 0136    i = pwd_len137    while i:138        a_ctx_update(db if i & 1 else pwd)139        i >>= 1140 141    # finish A142    da = a_ctx.digest()143 144    #===================================================================145    # digest P from password - used instead of password itself146    #                          when calculating digest C.147    #===================================================================148    if pwd_len < 96:149        # this method is faster under python, but uses O(pwd_len**2) memory;150        # so we don't use it for larger passwords to avoid a potential DOS.151        dp = repeat_string(hash_const(pwd * pwd_len).digest(), pwd_len)152    else:153        # this method is slower under python, but uses a fixed amount of memory.154        tmp_ctx = hash_const(pwd)155        tmp_ctx_update = tmp_ctx.update156        i = pwd_len-1157        while i:158            tmp_ctx_update(pwd)159            i -= 1160        dp = repeat_string(tmp_ctx.digest(), pwd_len)161    assert len(dp) == pwd_len162 163    #===================================================================164    # digest S  - used instead of salt itself when calculating digest C165    #===================================================================166    ds = hash_const(salt * (16 + byte_elem_value(da[0]))).digest()[:salt_len]167    assert len(ds) == salt_len, "salt_len somehow > hash_len!"168 169    #===================================================================170    # digest C - for a variable number of rounds, combine A, S, and P171    #            digests in various ways; in order to burn CPU time.172    #===================================================================173 174    # NOTE: the original SHA256/512-Crypt specification performs the C digest175    # calculation using the following loop:176    #177    ##dc = da178    ##i = 0179    ##while i < rounds:180    ##    tmp_ctx = hash_const(dp if i & 1 else dc)181    ##    if i % 3:182    ##        tmp_ctx.update(ds)183    ##    if i % 7:184    ##        tmp_ctx.update(dp)185    ##    tmp_ctx.update(dc if i & 1 else dp)186    ##    dc = tmp_ctx.digest()187    ##    i += 1188    #189    # The code Passlib uses (below) implements an equivalent algorithm,190    # it's just been heavily optimized to pre-calculate a large number191    # of things beforehand. It works off of a couple of observations192    # about the original algorithm:193    #194    # 1. each round is a combination of 'dc', 'ds', and 'dp'; determined195    #    by the whether 'i' a multiple of 2,3, and/or 7.196    # 2. since lcm(2,3,7)==42, the series of combinations will repeat197    #    every 42 rounds.198    # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';199    #    while odd rounds 1-41 consist of hash(round-specific-constant + dc)200    #201    # Using these observations, the following code...202    # * calculates the round-specific combination of ds & dp for each round 0-41203    # * runs through as many 42-round blocks as possible204    # * runs through as many pairs of rounds as possible for remaining rounds205    # * performs once last round if the total rounds should be odd.206    #207    # this cuts out a lot of the control overhead incurred when running the208    # original loop 40,000+ times in python, resulting in ~20% increase in209    # speed under CPython (though still 2x slower than glibc crypt)210 211    # prepare the 6 combinations of ds & dp which are needed212    # (order of 'perms' must match how _c_digest_offsets was generated)213    dp_dp = dp+dp214    dp_ds = dp+ds215    perms = [dp, dp_dp, dp_ds, dp_ds+dp, ds+dp, ds+dp_dp]216 217    # build up list of even-round & odd-round constants,218    # and store in 21-element list as (even,odd) pairs.219    data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]220 221    # perform as many full 42-round blocks as possible222    dc = da223    blocks, tail = divmod(rounds, 42)224    while blocks:225        for even, odd in data:226            dc = hash_const(odd + hash_const(dc + even).digest()).digest()227        blocks -= 1228 229    # perform any leftover rounds230    if tail:231        # perform any pairs of rounds232        pairs = tail>>1233        for even, odd in data[:pairs]:234            dc = hash_const(odd + hash_const(dc + even).digest()).digest()235 236        # if rounds was odd, do one last round (since we started at 0,237        # last round will be an even-numbered round)238        if tail & 1:239            dc = hash_const(dc + data[pairs][0]).digest()240 241    #===================================================================242    # encode digest using appropriate transpose map243    #===================================================================244    return h64.encode_transposed_bytes(dc, transpose_map).decode("ascii")245 246#=============================================================================247# handlers248#=============================================================================249_UROUNDS = u("rounds=")250_UDOLLAR = u("$")251_UZERO = u("0")252 253class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt,254                   uh.GenericHandler):255    """class containing common code shared by sha256_crypt & sha512_crypt"""256    #===================================================================257    # class attrs258    #===================================================================259    # name - set by subclass260    setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size")261    # ident - set by subclass262    checksum_chars = uh.HASH64_CHARS263    # checksum_size - set by subclass264 265    max_salt_size = 16266    salt_chars = uh.HASH64_CHARS267 268    min_rounds = 1000 # bounds set by spec269    max_rounds = 999999999 # bounds set by spec270    rounds_cost = "linear"271 272    _cdb_use_512 = False # flag for _calc_digest_builtin()273    _rounds_prefix = None # ident + _UROUNDS274 275    #===================================================================276    # methods277    #===================================================================278    implicit_rounds = False279 280    def __init__(self, implicit_rounds=None, **kwds):281        super(_SHA2_Common, self).__init__(**kwds)282        # if user calls hash() w/ 5000 rounds, default to compact form.283        if implicit_rounds is None:284            implicit_rounds = (self.use_defaults and self.rounds == 5000)285        self.implicit_rounds = implicit_rounds286 287    def _parse_salt(self, salt):288        # required per SHA2-crypt spec -- truncate config salts rather than throwing error289        return self._norm_salt(salt, relaxed=self.checksum is None)290 291    def _parse_rounds(self, rounds):292        # required per SHA2-crypt spec -- clip config rounds rather than throwing error293        return self._norm_rounds(rounds, relaxed=self.checksum is None)294 295    @classmethod296    def from_string(cls, hash):297        # basic format this parses -298        # $5$[rounds=<rounds>$]<salt>[$<checksum>]299 300        # TODO: this *could* use uh.parse_mc3(), except that the rounds301        # portion has a slightly different grammar.302 303        # convert to unicode, check for ident prefix, split on dollar signs.304        hash = to_unicode(hash, "ascii", "hash")305        ident = cls.ident306        if not hash.startswith(ident):307            raise uh.exc.InvalidHashError(cls)308        assert len(ident) == 3309        parts = hash[3:].split(_UDOLLAR)310 311        # extract rounds value312        if parts[0].startswith(_UROUNDS):313            assert len(_UROUNDS) == 7314            rounds = parts.pop(0)[7:]315            if rounds.startswith(_UZERO) and rounds != _UZERO:316                raise uh.exc.ZeroPaddedRoundsError(cls)317            rounds = int(rounds)318            implicit_rounds = False319        else:320            rounds = 5000321            implicit_rounds = True322 323        # rest should be salt and checksum324        if len(parts) == 2:325            salt, chk = parts326        elif len(parts) == 1:327            salt = parts[0]328            chk = None329        else:330            raise uh.exc.MalformedHashError(cls)331 332        # return new object333        return cls(334            rounds=rounds,335            salt=salt,336            checksum=chk or None,337            implicit_rounds=implicit_rounds,338            )339 340    def to_string(self):341        if self.rounds == 5000 and self.implicit_rounds:342            hash = u("%s%s$%s") % (self.ident, self.salt,343                                   self.checksum or u(''))344        else:345            hash = u("%srounds=%d$%s$%s") % (self.ident, self.rounds,346                                             self.salt, self.checksum or u(''))347        return uascii_to_str(hash)348 349    #===================================================================350    # backends351    #===================================================================352    backends = ("os_crypt", "builtin")353 354    #---------------------------------------------------------------355    # os_crypt backend356    #---------------------------------------------------------------357 358    #: test hash for OS detection -- provided by subclass359    _test_hash = None360 361    @classmethod362    def _load_backend_os_crypt(cls):363        if test_crypt(*cls._test_hash):364            cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)365            return True366        else:367            return False368 369    def _calc_checksum_os_crypt(self, secret):370        config = self.to_string()371        hash = safe_crypt(secret, config)372        if hash is None:373            # py3's crypt.crypt() can't handle non-utf8 bytes.374            # fallback to builtin alg, which is always available.375            return self._calc_checksum_builtin(secret)376        # NOTE: avoiding full parsing routine via from_string().checksum,377        # and just extracting the bit we need.378        cs = self.checksum_size379        if not hash.startswith(self.ident) or hash[-cs-1] != _UDOLLAR:380            raise uh.exc.CryptBackendError(self, config, hash)381        return hash[-cs:]382 383    #---------------------------------------------------------------384    # builtin backend385    #---------------------------------------------------------------386    @classmethod387    def _load_backend_builtin(cls):388        cls._set_calc_checksum_backend(cls._calc_checksum_builtin)389        return True390 391    def _calc_checksum_builtin(self, secret):392        return _raw_sha2_crypt(secret, self.salt, self.rounds,393                               self._cdb_use_512)394 395    #===================================================================396    # eoc397    #===================================================================398 399class sha256_crypt(_SHA2_Common):400    """This class implements the SHA256-Crypt password hash, and follows the :ref:`password-hash-api`.401 402    It supports a variable-length salt, and a variable number of rounds.403 404    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:405 406    :type salt: str407    :param salt:408        Optional salt string.409        If not specified, one will be autogenerated (this is recommended).410        If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``.411 412    :type rounds: int413    :param rounds:414        Optional number of rounds to use.415        Defaults to 535000, must be between 1000 and 999999999, inclusive.416 417        .. note::418            per the official specification, when the rounds parameter is set to 5000,419            it may be omitted from the hash string.420 421    :type relaxed: bool422    :param relaxed:423        By default, providing an invalid value for one of the other424        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,425        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`426        will be issued instead. Correctable errors include ``rounds``427        that are too small or too large, and ``salt`` strings that are too long.428 429        .. versionadded:: 1.6430 431    ..432        commented out, currently only supported by :meth:`hash`, and not via :meth:`using`:433 434        :type implicit_rounds: bool435        :param implicit_rounds:436            this is an internal option which generally doesn't need to be touched.437 438            this flag determines whether the hash should omit the rounds parameter439            when encoding it to a string; this is only permitted by the spec for rounds=5000,440            and the flag is ignored otherwise. the spec requires the two different441            encodings be preserved as they are, instead of normalizing them.442    """443    #===================================================================444    # class attrs445    #===================================================================446    name = "sha256_crypt"447    ident = u("$5$")448    checksum_size = 43449    # NOTE: using 25/75 weighting of builtin & os_crypt backends450    default_rounds = 535000451 452    #===================================================================453    # backends454    #===================================================================455    _test_hash = ("test", "$5$rounds=1000$test$QmQADEXMG8POI5W"456                          "Dsaeho0P36yK3Tcrgboabng6bkb/")457 458    #===================================================================459    # eoc460    #===================================================================461 462#=============================================================================463# sha 512 crypt464#=============================================================================465class sha512_crypt(_SHA2_Common):466    """This class implements the SHA512-Crypt password hash, and follows the :ref:`password-hash-api`.467 468    It supports a variable-length salt, and a variable number of rounds.469 470    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:471 472    :type salt: str473    :param salt:474        Optional salt string.475        If not specified, one will be autogenerated (this is recommended).476        If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``.477 478    :type rounds: int479    :param rounds:480        Optional number of rounds to use.481        Defaults to 656000, must be between 1000 and 999999999, inclusive.482 483        .. note::484            per the official specification, when the rounds parameter is set to 5000,485            it may be omitted from the hash string.486 487    :type relaxed: bool488    :param relaxed:489        By default, providing an invalid value for one of the other490        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,491        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`492        will be issued instead. Correctable errors include ``rounds``493        that are too small or too large, and ``salt`` strings that are too long.494 495        .. versionadded:: 1.6496 497    ..498        commented out, currently only supported by :meth:`hash`, and not via :meth:`using`:499 500        :type implicit_rounds: bool501        :param implicit_rounds:502            this is an internal option which generally doesn't need to be touched.503 504            this flag determines whether the hash should omit the rounds parameter505            when encoding it to a string; this is only permitted by the spec for rounds=5000,506            and the flag is ignored otherwise. the spec requires the two different507            encodings be preserved as they are, instead of normalizing them.508    """509 510    #===================================================================511    # class attrs512    #===================================================================513    name = "sha512_crypt"514    ident = u("$6$")515    checksum_size = 86516    _cdb_use_512 = True517    # NOTE: using 25/75 weighting of builtin & os_crypt backends518    default_rounds = 656000519 520    #===================================================================521    # backend522    #===================================================================523    _test_hash = ("test", "$6$rounds=1000$test$2M/Lx6Mtobqj"524                          "Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn"525                          "yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG"526                          "I5c7TZauS0")527 528    #===================================================================529    # eoc530    #===================================================================531 532#=============================================================================533# eof534#=============================================================================535 
codekingpro/portable-devtools · Team Ai