codekingpro/portable-devtools
114k
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 