You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

384 line
14 KiB

  1. """passlib.handlers.scrypt -- scrypt password hash"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import with_statement, absolute_import
  6. # core
  7. import logging; log = logging.getLogger(__name__)
  8. # site
  9. # pkg
  10. from passlib.crypto import scrypt as _scrypt
  11. from passlib.utils import h64, to_bytes
  12. from passlib.utils.binary import h64, b64s_decode, b64s_encode
  13. from passlib.utils.compat import u, bascii_to_str, suppress_cause
  14. from passlib.utils.decor import classproperty
  15. import passlib.utils.handlers as uh
  16. # local
  17. __all__ = [
  18. "scrypt",
  19. ]
  20. #=============================================================================
  21. # scrypt format identifiers
  22. #=============================================================================
  23. IDENT_SCRYPT = u("$scrypt$") # identifier used by passlib
  24. IDENT_7 = u("$7$") # used by official scrypt spec
  25. _UDOLLAR = u("$")
  26. #=============================================================================
  27. # handler
  28. #=============================================================================
  29. class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents,
  30. uh.GenericHandler):
  31. """This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`.
  32. It supports a variable-length salt, a variable number of rounds,
  33. as well as some custom tuning parameters unique to scrypt (see below).
  34. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  35. :type salt: str
  36. :param salt:
  37. Optional salt string.
  38. If specified, the length must be between 0-1024 bytes.
  39. If not specified, one will be auto-generated (this is recommended).
  40. :type salt_size: int
  41. :param salt_size:
  42. Optional number of bytes to use when autogenerating new salts.
  43. Defaults to 16 bytes, but can be any value between 0 and 1024.
  44. :type rounds: int
  45. :param rounds:
  46. Optional number of rounds to use.
  47. Defaults to 16, but must be within ``range(1,32)``.
  48. .. warning::
  49. Unlike many hash algorithms, increasing the rounds value
  50. will increase both the time *and memory* required to hash a password.
  51. :type block_size: int
  52. :param block_size:
  53. Optional block size to pass to scrypt hash function (the ``r`` parameter).
  54. Useful for tuning scrypt to optimal performance for your CPU architecture.
  55. Defaults to 8.
  56. :type parallelism: int
  57. :param parallelism:
  58. Optional parallelism to pass to scrypt hash function (the ``p`` parameter).
  59. Defaults to 1.
  60. :type relaxed: bool
  61. :param relaxed:
  62. By default, providing an invalid value for one of the other
  63. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  64. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  65. will be issued instead. Correctable errors include ``rounds``
  66. that are too small or too large, and ``salt`` strings that are too long.
  67. .. note::
  68. The underlying scrypt hash function has a number of limitations
  69. on it's parameter values, which forbids certain combinations of settings.
  70. The requirements are:
  71. * ``linear_rounds = 2**<some positive integer>``
  72. * ``linear_rounds < 2**(16 * block_size)``
  73. * ``block_size * parallelism <= 2**30-1``
  74. .. todo::
  75. This class currently does not support configuring default values
  76. for ``block_size`` or ``parallelism`` via a :class:`~passlib.context.CryptContext`
  77. configuration.
  78. """
  79. #===================================================================
  80. # class attrs
  81. #===================================================================
  82. #------------------------
  83. # PasswordHash
  84. #------------------------
  85. name = "scrypt"
  86. setting_kwds = ("ident", "salt", "salt_size", "rounds", "block_size", "parallelism")
  87. #------------------------
  88. # GenericHandler
  89. #------------------------
  90. # NOTE: scrypt supports arbitrary output sizes. since it's output runs through
  91. # pbkdf2-hmac-sha256 before returning, and this could be raised eventually...
  92. # but a 256-bit digest is more than sufficient for password hashing.
  93. # XXX: make checksum size configurable? could merge w/ argon2 code that does this.
  94. checksum_size = 32
  95. #------------------------
  96. # HasManyIdents
  97. #------------------------
  98. default_ident = IDENT_SCRYPT
  99. ident_values = (IDENT_SCRYPT, IDENT_7)
  100. #------------------------
  101. # HasRawSalt
  102. #------------------------
  103. default_salt_size = 16
  104. max_salt_size = 1024
  105. #------------------------
  106. # HasRounds
  107. #------------------------
  108. # TODO: would like to dynamically pick this based on system
  109. default_rounds = 16
  110. min_rounds = 1
  111. max_rounds = 31 # limited by scrypt alg
  112. rounds_cost = "log2"
  113. # TODO: make default block size configurable via using(), and deprecatable via .needs_update()
  114. #===================================================================
  115. # instance attrs
  116. #===================================================================
  117. #: default parallelism setting (min=1 currently hardcoded in mixin)
  118. parallelism = 1
  119. #: default block size setting
  120. block_size = 8
  121. #===================================================================
  122. # variant constructor
  123. #===================================================================
  124. @classmethod
  125. def using(cls, block_size=None, **kwds):
  126. subcls = super(scrypt, cls).using(**kwds)
  127. if block_size is not None:
  128. if isinstance(block_size, uh.native_string_types):
  129. block_size = int(block_size)
  130. subcls.block_size = subcls._norm_block_size(block_size, relaxed=kwds.get("relaxed"))
  131. # make sure param combination is valid for scrypt()
  132. try:
  133. _scrypt.validate(1 << cls.default_rounds, cls.block_size, cls.parallelism)
  134. except ValueError as err:
  135. raise suppress_cause(ValueError("scrypt: invalid settings combination: " + str(err)))
  136. return subcls
  137. #===================================================================
  138. # parsing
  139. #===================================================================
  140. @classmethod
  141. def from_string(cls, hash):
  142. return cls(**cls.parse(hash))
  143. @classmethod
  144. def parse(cls, hash):
  145. ident, suffix = cls._parse_ident(hash)
  146. func = getattr(cls, "_parse_%s_string" % ident.strip(_UDOLLAR), None)
  147. if func:
  148. return func(suffix)
  149. else:
  150. raise uh.exc.InvalidHashError(cls)
  151. #
  152. # passlib's format:
  153. # $scrypt$ln=<logN>,r=<r>,p=<p>$<salt>[$<digest>]
  154. # where:
  155. # logN, r, p -- decimal-encoded positive integer, no zero-padding
  156. # logN -- log cost setting
  157. # r -- block size setting (usually 8)
  158. # p -- parallelism setting (usually 1)
  159. # salt, digest -- b64-nopad encoded bytes
  160. #
  161. @classmethod
  162. def _parse_scrypt_string(cls, suffix):
  163. # break params, salt, and digest sections
  164. parts = suffix.split("$")
  165. if len(parts) == 3:
  166. params, salt, digest = parts
  167. elif len(parts) == 2:
  168. params, salt = parts
  169. digest = None
  170. else:
  171. raise uh.exc.MalformedHashError(cls, "malformed hash")
  172. # break params apart
  173. parts = params.split(",")
  174. if len(parts) == 3:
  175. nstr, bstr, pstr = parts
  176. assert nstr.startswith("ln=")
  177. assert bstr.startswith("r=")
  178. assert pstr.startswith("p=")
  179. else:
  180. raise uh.exc.MalformedHashError(cls, "malformed settings field")
  181. return dict(
  182. ident=IDENT_SCRYPT,
  183. rounds=int(nstr[3:]),
  184. block_size=int(bstr[2:]),
  185. parallelism=int(pstr[2:]),
  186. salt=b64s_decode(salt.encode("ascii")),
  187. checksum=b64s_decode(digest.encode("ascii")) if digest else None,
  188. )
  189. #
  190. # official format specification defined at
  191. # https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt
  192. # format:
  193. # $7$<N><rrrrr><ppppp><salt...>[$<digest>]
  194. # 0 12345 67890 1
  195. # where:
  196. # All bytes use h64-little-endian encoding
  197. # N: 6-bit log cost setting
  198. # r: 30-bit block size setting
  199. # p: 30-bit parallelism setting
  200. # salt: variable length salt bytes
  201. # digest: fixed 32-byte digest
  202. #
  203. @classmethod
  204. def _parse_7_string(cls, suffix):
  205. # XXX: annoyingly, official spec embeds salt *raw*, yet doesn't specify a hash encoding.
  206. # so assuming only h64 chars are valid for salt, and are ASCII encoded.
  207. # split into params & digest
  208. parts = suffix.encode("ascii").split(b"$")
  209. if len(parts) == 2:
  210. params, digest = parts
  211. elif len(parts) == 1:
  212. params, = parts
  213. digest = None
  214. else:
  215. raise uh.exc.MalformedHashError()
  216. # parse params & return
  217. if len(params) < 11:
  218. raise uh.exc.MalformedHashError(cls, "params field too short")
  219. return dict(
  220. ident=IDENT_7,
  221. rounds=h64.decode_int6(params[:1]),
  222. block_size=h64.decode_int30(params[1:6]),
  223. parallelism=h64.decode_int30(params[6:11]),
  224. salt=params[11:],
  225. checksum=h64.decode_bytes(digest) if digest else None,
  226. )
  227. #===================================================================
  228. # formatting
  229. #===================================================================
  230. def to_string(self):
  231. ident = self.ident
  232. if ident == IDENT_SCRYPT:
  233. return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % (
  234. self.rounds,
  235. self.block_size,
  236. self.parallelism,
  237. bascii_to_str(b64s_encode(self.salt)),
  238. bascii_to_str(b64s_encode(self.checksum)),
  239. )
  240. else:
  241. assert ident == IDENT_7
  242. salt = self.salt
  243. try:
  244. salt.decode("ascii")
  245. except UnicodeDecodeError:
  246. raise suppress_cause(NotImplementedError("scrypt $7$ hashes dont support non-ascii salts"))
  247. return bascii_to_str(b"".join([
  248. b"$7$",
  249. h64.encode_int6(self.rounds),
  250. h64.encode_int30(self.block_size),
  251. h64.encode_int30(self.parallelism),
  252. self.salt,
  253. b"$",
  254. h64.encode_bytes(self.checksum)
  255. ]))
  256. #===================================================================
  257. # init
  258. #===================================================================
  259. def __init__(self, block_size=None, **kwds):
  260. super(scrypt, self).__init__(**kwds)
  261. # init block size
  262. if block_size is None:
  263. assert uh.validate_default_value(self, self.block_size, self._norm_block_size,
  264. param="block_size")
  265. else:
  266. self.block_size = self._norm_block_size(block_size)
  267. # NOTE: if hash contains invalid complex constraint, relying on error
  268. # being raised by scrypt call in _calc_checksum()
  269. @classmethod
  270. def _norm_block_size(cls, block_size, relaxed=False):
  271. return uh.norm_integer(cls, block_size, min=1, param="block_size", relaxed=relaxed)
  272. def _generate_salt(self):
  273. salt = super(scrypt, self)._generate_salt()
  274. if self.ident == IDENT_7:
  275. # this format doesn't support non-ascii salts.
  276. # as workaround, we take raw bytes, encoded to base64
  277. salt = b64s_encode(salt)
  278. return salt
  279. #===================================================================
  280. # backend configuration
  281. # NOTE: this following HasManyBackends' API, but provides it's own implementation,
  282. # which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses.
  283. #===================================================================
  284. @classproperty
  285. def backends(cls):
  286. return _scrypt.backend_values
  287. @classmethod
  288. def get_backend(cls):
  289. return _scrypt.backend
  290. @classmethod
  291. def has_backend(cls, name="any"):
  292. try:
  293. cls.set_backend(name, dryrun=True)
  294. return True
  295. except uh.exc.MissingBackendError:
  296. return False
  297. @classmethod
  298. def set_backend(cls, name="any", dryrun=False):
  299. _scrypt._set_backend(name, dryrun=dryrun)
  300. #===================================================================
  301. # digest calculation
  302. #===================================================================
  303. def _calc_checksum(self, secret):
  304. secret = to_bytes(secret, param="secret")
  305. return _scrypt.scrypt(secret, self.salt, n=(1 << self.rounds), r=self.block_size,
  306. p=self.parallelism, keylen=self.checksum_size)
  307. #===================================================================
  308. # hash migration
  309. #===================================================================
  310. def _calc_needs_update(self, **kwds):
  311. """
  312. mark hash as needing update if rounds is outside desired bounds.
  313. """
  314. # XXX: for now, marking all hashes which don't have matching block_size setting
  315. if self.block_size != type(self).block_size:
  316. return True
  317. return super(scrypt, self)._calc_needs_update(**kwds)
  318. #===================================================================
  319. # eoc
  320. #===================================================================
  321. #=============================================================================
  322. # eof
  323. #=============================================================================