"""passlib.handlers.scrypt -- scrypt password hash""" #============================================================================= # imports #============================================================================= from __future__ import with_statement, absolute_import # core import logging; log = logging.getLogger(__name__) # site # pkg from passlib.crypto import scrypt as _scrypt from passlib.utils import h64, to_bytes from passlib.utils.binary import h64, b64s_decode, b64s_encode from passlib.utils.compat import u, bascii_to_str, suppress_cause from passlib.utils.decor import classproperty import passlib.utils.handlers as uh # local __all__ = [ "scrypt", ] #============================================================================= # scrypt format identifiers #============================================================================= IDENT_SCRYPT = u("$scrypt$") # identifier used by passlib IDENT_7 = u("$7$") # used by official scrypt spec _UDOLLAR = u("$") #============================================================================= # handler #============================================================================= class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents, uh.GenericHandler): """This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, a variable number of rounds, as well as some custom tuning parameters unique to scrypt (see below). The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If specified, the length must be between 0-1024 bytes. If not specified, one will be auto-generated (this is recommended). :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 16 bytes, but can be any value between 0 and 1024. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 16, but must be within ``range(1,32)``. .. warning:: Unlike many hash algorithms, increasing the rounds value will increase both the time *and memory* required to hash a password. :type block_size: int :param block_size: Optional block size to pass to scrypt hash function (the ``r`` parameter). Useful for tuning scrypt to optimal performance for your CPU architecture. Defaults to 8. :type parallelism: int :param parallelism: Optional parallelism to pass to scrypt hash function (the ``p`` parameter). Defaults to 1. :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. .. note:: The underlying scrypt hash function has a number of limitations on it's parameter values, which forbids certain combinations of settings. The requirements are: * ``linear_rounds = 2**`` * ``linear_rounds < 2**(16 * block_size)`` * ``block_size * parallelism <= 2**30-1`` .. todo:: This class currently does not support configuring default values for ``block_size`` or ``parallelism`` via a :class:`~passlib.context.CryptContext` configuration. """ #=================================================================== # class attrs #=================================================================== #------------------------ # PasswordHash #------------------------ name = "scrypt" setting_kwds = ("ident", "salt", "salt_size", "rounds", "block_size", "parallelism") #------------------------ # GenericHandler #------------------------ # NOTE: scrypt supports arbitrary output sizes. since it's output runs through # pbkdf2-hmac-sha256 before returning, and this could be raised eventually... # but a 256-bit digest is more than sufficient for password hashing. # XXX: make checksum size configurable? could merge w/ argon2 code that does this. checksum_size = 32 #------------------------ # HasManyIdents #------------------------ default_ident = IDENT_SCRYPT ident_values = (IDENT_SCRYPT, IDENT_7) #------------------------ # HasRawSalt #------------------------ default_salt_size = 16 max_salt_size = 1024 #------------------------ # HasRounds #------------------------ # TODO: would like to dynamically pick this based on system default_rounds = 16 min_rounds = 1 max_rounds = 31 # limited by scrypt alg rounds_cost = "log2" # TODO: make default block size configurable via using(), and deprecatable via .needs_update() #=================================================================== # instance attrs #=================================================================== #: default parallelism setting (min=1 currently hardcoded in mixin) parallelism = 1 #: default block size setting block_size = 8 #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, block_size=None, **kwds): subcls = super(scrypt, cls).using(**kwds) if block_size is not None: if isinstance(block_size, uh.native_string_types): block_size = int(block_size) subcls.block_size = subcls._norm_block_size(block_size, relaxed=kwds.get("relaxed")) # make sure param combination is valid for scrypt() try: _scrypt.validate(1 << cls.default_rounds, cls.block_size, cls.parallelism) except ValueError as err: raise suppress_cause(ValueError("scrypt: invalid settings combination: " + str(err))) return subcls #=================================================================== # parsing #=================================================================== @classmethod def from_string(cls, hash): return cls(**cls.parse(hash)) @classmethod def parse(cls, hash): ident, suffix = cls._parse_ident(hash) func = getattr(cls, "_parse_%s_string" % ident.strip(_UDOLLAR), None) if func: return func(suffix) else: raise uh.exc.InvalidHashError(cls) # # passlib's format: # $scrypt$ln=,r=,p=

$[$] # where: # logN, r, p -- decimal-encoded positive integer, no zero-padding # logN -- log cost setting # r -- block size setting (usually 8) # p -- parallelism setting (usually 1) # salt, digest -- b64-nopad encoded bytes # @classmethod def _parse_scrypt_string(cls, suffix): # break params, salt, and digest sections parts = suffix.split("$") if len(parts) == 3: params, salt, digest = parts elif len(parts) == 2: params, salt = parts digest = None else: raise uh.exc.MalformedHashError(cls, "malformed hash") # break params apart parts = params.split(",") if len(parts) == 3: nstr, bstr, pstr = parts assert nstr.startswith("ln=") assert bstr.startswith("r=") assert pstr.startswith("p=") else: raise uh.exc.MalformedHashError(cls, "malformed settings field") return dict( ident=IDENT_SCRYPT, rounds=int(nstr[3:]), block_size=int(bstr[2:]), parallelism=int(pstr[2:]), salt=b64s_decode(salt.encode("ascii")), checksum=b64s_decode(digest.encode("ascii")) if digest else None, ) # # official format specification defined at # https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt # format: # $7$[$] # 0 12345 67890 1 # where: # All bytes use h64-little-endian encoding # N: 6-bit log cost setting # r: 30-bit block size setting # p: 30-bit parallelism setting # salt: variable length salt bytes # digest: fixed 32-byte digest # @classmethod def _parse_7_string(cls, suffix): # XXX: annoyingly, official spec embeds salt *raw*, yet doesn't specify a hash encoding. # so assuming only h64 chars are valid for salt, and are ASCII encoded. # split into params & digest parts = suffix.encode("ascii").split(b"$") if len(parts) == 2: params, digest = parts elif len(parts) == 1: params, = parts digest = None else: raise uh.exc.MalformedHashError() # parse params & return if len(params) < 11: raise uh.exc.MalformedHashError(cls, "params field too short") return dict( ident=IDENT_7, rounds=h64.decode_int6(params[:1]), block_size=h64.decode_int30(params[1:6]), parallelism=h64.decode_int30(params[6:11]), salt=params[11:], checksum=h64.decode_bytes(digest) if digest else None, ) #=================================================================== # formatting #=================================================================== def to_string(self): ident = self.ident if ident == IDENT_SCRYPT: return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % ( self.rounds, self.block_size, self.parallelism, bascii_to_str(b64s_encode(self.salt)), bascii_to_str(b64s_encode(self.checksum)), ) else: assert ident == IDENT_7 salt = self.salt try: salt.decode("ascii") except UnicodeDecodeError: raise suppress_cause(NotImplementedError("scrypt $7$ hashes dont support non-ascii salts")) return bascii_to_str(b"".join([ b"$7$", h64.encode_int6(self.rounds), h64.encode_int30(self.block_size), h64.encode_int30(self.parallelism), self.salt, b"$", h64.encode_bytes(self.checksum) ])) #=================================================================== # init #=================================================================== def __init__(self, block_size=None, **kwds): super(scrypt, self).__init__(**kwds) # init block size if block_size is None: assert uh.validate_default_value(self, self.block_size, self._norm_block_size, param="block_size") else: self.block_size = self._norm_block_size(block_size) # NOTE: if hash contains invalid complex constraint, relying on error # being raised by scrypt call in _calc_checksum() @classmethod def _norm_block_size(cls, block_size, relaxed=False): return uh.norm_integer(cls, block_size, min=1, param="block_size", relaxed=relaxed) def _generate_salt(self): salt = super(scrypt, self)._generate_salt() if self.ident == IDENT_7: # this format doesn't support non-ascii salts. # as workaround, we take raw bytes, encoded to base64 salt = b64s_encode(salt) return salt #=================================================================== # backend configuration # NOTE: this following HasManyBackends' API, but provides it's own implementation, # which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses. #=================================================================== @classproperty def backends(cls): return _scrypt.backend_values @classmethod def get_backend(cls): return _scrypt.backend @classmethod def has_backend(cls, name="any"): try: cls.set_backend(name, dryrun=True) return True except uh.exc.MissingBackendError: return False @classmethod def set_backend(cls, name="any", dryrun=False): _scrypt._set_backend(name, dryrun=dryrun) #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): secret = to_bytes(secret, param="secret") return _scrypt.scrypt(secret, self.salt, n=(1 << self.rounds), r=self.block_size, p=self.parallelism, keylen=self.checksum_size) #=================================================================== # hash migration #=================================================================== def _calc_needs_update(self, **kwds): """ mark hash as needing update if rounds is outside desired bounds. """ # XXX: for now, marking all hashes which don't have matching block_size setting if self.block_size != type(self).block_size: return True return super(scrypt, self)._calc_needs_update(**kwds) #=================================================================== # eoc #=================================================================== #============================================================================= # eof #=============================================================================