Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
scrypt.py384 linesDownload Raw Back to handlers
1"""passlib.handlers.scrypt -- scrypt password hash"""2#=============================================================================3# imports4#=============================================================================5from __future__ import with_statement, absolute_import6# core7import logging; log = logging.getLogger(__name__)8# site9# pkg10from passlib.crypto import scrypt as _scrypt11from passlib.utils import h64, to_bytes12from passlib.utils.binary import h64, b64s_decode, b64s_encode13from passlib.utils.compat import u, bascii_to_str, suppress_cause14from passlib.utils.decor import classproperty15import passlib.utils.handlers as uh16# local17__all__ = [18    "scrypt",19]20 21#=============================================================================22# scrypt format identifiers23#=============================================================================24 25IDENT_SCRYPT = u("$scrypt$")  # identifier used by passlib26IDENT_7 = u("$7$")  # used by official scrypt spec27 28_UDOLLAR = u("$")29 30#=============================================================================31# handler32#=============================================================================33class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents,34             uh.GenericHandler):35    """This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`.36 37    It supports a variable-length salt, a variable number of rounds,38    as well as some custom tuning parameters unique to scrypt (see below).39 40    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:41 42    :type salt: str43    :param salt:44        Optional salt string.45        If specified, the length must be between 0-1024 bytes.46        If not specified, one will be auto-generated (this is recommended).47 48    :type salt_size: int49    :param salt_size:50        Optional number of bytes to use when autogenerating new salts.51        Defaults to 16 bytes, but can be any value between 0 and 1024.52 53    :type rounds: int54    :param rounds:55        Optional number of rounds to use.56        Defaults to 16, but must be within ``range(1,32)``.57 58        .. warning::59 60            Unlike many hash algorithms, increasing the rounds value61            will increase both the time *and memory* required to hash a password.62 63    :type block_size: int64    :param block_size:65        Optional block size to pass to scrypt hash function (the ``r`` parameter).66        Useful for tuning scrypt to optimal performance for your CPU architecture.67        Defaults to 8.68 69    :type parallelism: int70    :param parallelism:71        Optional parallelism to pass to scrypt hash function (the ``p`` parameter).72        Defaults to 1.73 74    :type relaxed: bool75    :param relaxed:76        By default, providing an invalid value for one of the other77        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,78        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`79        will be issued instead. Correctable errors include ``rounds``80        that are too small or too large, and ``salt`` strings that are too long.81 82    .. note::83 84        The underlying scrypt hash function has a number of limitations85        on it's parameter values, which forbids certain combinations of settings.86        The requirements are:87 88        * ``linear_rounds = 2**<some positive integer>``89        * ``linear_rounds < 2**(16 * block_size)``90        * ``block_size * parallelism <= 2**30-1``91 92    .. todo::93 94        This class currently does not support configuring default values95        for ``block_size`` or ``parallelism`` via a :class:`~passlib.context.CryptContext`96        configuration.97    """98 99    #===================================================================100    # class attrs101    #===================================================================102 103    #------------------------104    # PasswordHash105    #------------------------106    name = "scrypt"107    setting_kwds = ("ident", "salt", "salt_size", "rounds", "block_size", "parallelism")108 109    #------------------------110    # GenericHandler111    #------------------------112    # NOTE: scrypt supports arbitrary output sizes. since it's output runs through113    #       pbkdf2-hmac-sha256 before returning, and this could be raised eventually...114    #       but a 256-bit digest is more than sufficient for password hashing.115    # XXX: make checksum size configurable? could merge w/ argon2 code that does this.116    checksum_size = 32117 118    #------------------------119    # HasManyIdents120    #------------------------121    default_ident = IDENT_SCRYPT122    ident_values = (IDENT_SCRYPT, IDENT_7)123 124    #------------------------125    # HasRawSalt126    #------------------------127    default_salt_size = 16128    max_salt_size = 1024129 130    #------------------------131    # HasRounds132    #------------------------133    # TODO: would like to dynamically pick this based on system134    default_rounds = 16135    min_rounds = 1136    max_rounds = 31  # limited by scrypt alg137    rounds_cost = "log2"138 139    # TODO: make default block size configurable via using(), and deprecatable via .needs_update()140 141    #===================================================================142    # instance attrs143    #===================================================================144 145    #: default parallelism setting (min=1 currently hardcoded in mixin)146    parallelism = 1147 148    #: default block size setting149    block_size = 8150 151    #===================================================================152    # variant constructor153    #===================================================================154 155    @classmethod156    def using(cls, block_size=None, **kwds):157        subcls = super(scrypt, cls).using(**kwds)158        if block_size is not None:159            if isinstance(block_size, uh.native_string_types):160                block_size = int(block_size)161            subcls.block_size = subcls._norm_block_size(block_size, relaxed=kwds.get("relaxed"))162 163        # make sure param combination is valid for scrypt()164        try:165            _scrypt.validate(1 << cls.default_rounds, cls.block_size, cls.parallelism)166        except ValueError as err:167            raise suppress_cause(ValueError("scrypt: invalid settings combination: " + str(err)))168 169        return subcls170 171    #===================================================================172    # parsing173    #===================================================================174 175    @classmethod176    def from_string(cls, hash):177        return cls(**cls.parse(hash))178 179    @classmethod180    def parse(cls, hash):181        ident, suffix = cls._parse_ident(hash)182        func = getattr(cls, "_parse_%s_string" % ident.strip(_UDOLLAR), None)183        if func:184            return func(suffix)185        else:186            raise uh.exc.InvalidHashError(cls)187 188    #189    # passlib's format:190    #   $scrypt$ln=<logN>,r=<r>,p=<p>$<salt>[$<digest>]191    # where:192    #   logN, r, p -- decimal-encoded positive integer, no zero-padding193    #   logN -- log cost setting194    #   r -- block size setting (usually 8)195    #   p -- parallelism setting (usually 1)196    #   salt, digest -- b64-nopad encoded bytes197    #198 199    @classmethod200    def _parse_scrypt_string(cls, suffix):201        # break params, salt, and digest sections202        parts = suffix.split("$")203        if len(parts) == 3:204            params, salt, digest = parts205        elif len(parts) == 2:206            params, salt = parts207            digest = None208        else:209            raise uh.exc.MalformedHashError(cls, "malformed hash")210 211        # break params apart212        parts = params.split(",")213        if len(parts) == 3:214            nstr, bstr, pstr = parts215            assert nstr.startswith("ln=")216            assert bstr.startswith("r=")217            assert pstr.startswith("p=")218        else:219            raise uh.exc.MalformedHashError(cls, "malformed settings field")220 221        return dict(222            ident=IDENT_SCRYPT,223            rounds=int(nstr[3:]),224            block_size=int(bstr[2:]),225            parallelism=int(pstr[2:]),226            salt=b64s_decode(salt.encode("ascii")),227            checksum=b64s_decode(digest.encode("ascii")) if digest else None,228            )229 230    #231    # official format specification defined at232    #   https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt233    # format:234    #   $7$<N><rrrrr><ppppp><salt...>[$<digest>]235    #       0  12345  67890  1236    # where:237    #   All bytes use h64-little-endian encoding238    #   N: 6-bit log cost setting239    #   r: 30-bit block size setting240    #   p: 30-bit parallelism setting241    #   salt: variable length salt bytes242    #   digest: fixed 32-byte digest243    #244 245    @classmethod246    def _parse_7_string(cls, suffix):247        # XXX: annoyingly, official spec embeds salt *raw*, yet doesn't specify a hash encoding.248        #      so assuming only h64 chars are valid for salt, and are ASCII encoded.249 250        # split into params & digest251        parts = suffix.encode("ascii").split(b"$")252        if len(parts) == 2:253            params, digest = parts254        elif len(parts) == 1:255            params, = parts256            digest = None257        else:258            raise uh.exc.MalformedHashError()259 260        # parse params & return261        if len(params) < 11:262            raise uh.exc.MalformedHashError(cls, "params field too short")263        return dict(264            ident=IDENT_7,265            rounds=h64.decode_int6(params[:1]),266            block_size=h64.decode_int30(params[1:6]),267            parallelism=h64.decode_int30(params[6:11]),268            salt=params[11:],269            checksum=h64.decode_bytes(digest) if digest else None,270        )271 272    #===================================================================273    # formatting274    #===================================================================275    def to_string(self):276        ident = self.ident277        if ident == IDENT_SCRYPT:278            return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % (279                self.rounds,280                self.block_size,281                self.parallelism,282                bascii_to_str(b64s_encode(self.salt)),283                bascii_to_str(b64s_encode(self.checksum)),284            )285        else:286            assert ident == IDENT_7287            salt = self.salt288            try:289                salt.decode("ascii")290            except UnicodeDecodeError:291                raise suppress_cause(NotImplementedError("scrypt $7$ hashes dont support non-ascii salts"))292            return bascii_to_str(b"".join([293                b"$7$",294                h64.encode_int6(self.rounds),295                h64.encode_int30(self.block_size),296                h64.encode_int30(self.parallelism),297                self.salt,298                b"$",299                h64.encode_bytes(self.checksum)300            ]))301 302    #===================================================================303    # init304    #===================================================================305    def __init__(self, block_size=None, **kwds):306        super(scrypt, self).__init__(**kwds)307 308        # init block size309        if block_size is None:310            assert uh.validate_default_value(self, self.block_size, self._norm_block_size,311                                             param="block_size")312        else:313            self.block_size = self._norm_block_size(block_size)314 315        # NOTE: if hash contains invalid complex constraint, relying on error316        #       being raised by scrypt call in _calc_checksum()317 318    @classmethod319    def _norm_block_size(cls, block_size, relaxed=False):320        return uh.norm_integer(cls, block_size, min=1, param="block_size", relaxed=relaxed)321 322    def _generate_salt(self):323        salt = super(scrypt, self)._generate_salt()324        if self.ident == IDENT_7:325            # this format doesn't support non-ascii salts.326            # as workaround, we take raw bytes, encoded to base64327            salt = b64s_encode(salt)328        return salt329 330    #===================================================================331    # backend configuration332    # NOTE: this following HasManyBackends' API, but provides it's own implementation,333    #       which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses.334    #===================================================================335 336    @classproperty337    def backends(cls):338        return _scrypt.backend_values339 340    @classmethod341    def get_backend(cls):342        return _scrypt.backend343 344    @classmethod345    def has_backend(cls, name="any"):346        try:347            cls.set_backend(name, dryrun=True)348            return True349        except uh.exc.MissingBackendError:350            return False351 352    @classmethod353    def set_backend(cls, name="any", dryrun=False):354        _scrypt._set_backend(name, dryrun=dryrun)355 356    #===================================================================357    # digest calculation358    #===================================================================359    def _calc_checksum(self, secret):360        secret = to_bytes(secret, param="secret")361        return _scrypt.scrypt(secret, self.salt, n=(1 << self.rounds), r=self.block_size,362                              p=self.parallelism, keylen=self.checksum_size)363 364    #===================================================================365    # hash migration366    #===================================================================367 368    def _calc_needs_update(self, **kwds):369        """370        mark hash as needing update if rounds is outside desired bounds.371        """372        # XXX: for now, marking all hashes which don't have matching block_size setting373        if self.block_size != type(self).block_size:374            return True375        return super(scrypt, self)._calc_needs_update(**kwds)376 377    #===================================================================378    # eoc379    #===================================================================380 381#=============================================================================382# eof383#=============================================================================384 
codekingpro/portable-devtools · Team Ai