您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

215 行
7.6 KiB

  1. """passlib.handlers.fshp
  2. """
  3. #=============================================================================
  4. # imports
  5. #=============================================================================
  6. # core
  7. from base64 import b64encode, b64decode
  8. import re
  9. import logging; log = logging.getLogger(__name__)
  10. # site
  11. # pkg
  12. from passlib.utils import to_unicode
  13. import passlib.utils.handlers as uh
  14. from passlib.utils.compat import bascii_to_str, iteritems, u,\
  15. unicode
  16. from passlib.crypto.digest import pbkdf1
  17. # local
  18. __all__ = [
  19. 'fshp',
  20. ]
  21. #=============================================================================
  22. # sha1-crypt
  23. #=============================================================================
  24. class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
  25. """This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.
  26. It supports a variable-length salt, and a variable number of rounds.
  27. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  28. :param salt:
  29. Optional raw salt string.
  30. If not specified, one will be autogenerated (this is recommended).
  31. :param salt_size:
  32. Optional number of bytes to use when autogenerating new salts.
  33. Defaults to 16 bytes, but can be any non-negative value.
  34. :param rounds:
  35. Optional number of rounds to use.
  36. Defaults to 480000, must be between 1 and 4294967295, inclusive.
  37. :param variant:
  38. Optionally specifies variant of FSHP to use.
  39. * ``0`` - uses SHA-1 digest (deprecated).
  40. * ``1`` - uses SHA-2/256 digest (default).
  41. * ``2`` - uses SHA-2/384 digest.
  42. * ``3`` - uses SHA-2/512 digest.
  43. :type relaxed: bool
  44. :param relaxed:
  45. By default, providing an invalid value for one of the other
  46. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  47. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  48. will be issued instead. Correctable errors include ``rounds``
  49. that are too small or too large, and ``salt`` strings that are too long.
  50. .. versionadded:: 1.6
  51. """
  52. #===================================================================
  53. # class attrs
  54. #===================================================================
  55. #--GenericHandler--
  56. name = "fshp"
  57. setting_kwds = ("salt", "salt_size", "rounds", "variant")
  58. checksum_chars = uh.PADDED_BASE64_CHARS
  59. ident = u("{FSHP")
  60. # checksum_size is property() that depends on variant
  61. #--HasRawSalt--
  62. default_salt_size = 16 # current passlib default, FSHP uses 8
  63. max_salt_size = None
  64. #--HasRounds--
  65. # FIXME: should probably use different default rounds
  66. # based on the variant. setting for default variant (sha256) for now.
  67. default_rounds = 480000 # current passlib default, FSHP uses 4096
  68. min_rounds = 1 # set by FSHP
  69. max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP
  70. rounds_cost = "linear"
  71. #--variants--
  72. default_variant = 1
  73. _variant_info = {
  74. # variant: (hash name, digest size)
  75. 0: ("sha1", 20),
  76. 1: ("sha256", 32),
  77. 2: ("sha384", 48),
  78. 3: ("sha512", 64),
  79. }
  80. _variant_aliases = dict(
  81. [(unicode(k),k) for k in _variant_info] +
  82. [(v[0],k) for k,v in iteritems(_variant_info)]
  83. )
  84. #===================================================================
  85. # configuration
  86. #===================================================================
  87. @classmethod
  88. def using(cls, variant=None, **kwds):
  89. subcls = super(fshp, cls).using(**kwds)
  90. if variant is not None:
  91. subcls.default_variant = cls._norm_variant(variant)
  92. return subcls
  93. #===================================================================
  94. # instance attrs
  95. #===================================================================
  96. variant = None
  97. #===================================================================
  98. # init
  99. #===================================================================
  100. def __init__(self, variant=None, **kwds):
  101. # NOTE: variant must be set first, since it controls checksum size, etc.
  102. self.use_defaults = kwds.get("use_defaults") # load this early
  103. if variant is not None:
  104. variant = self._norm_variant(variant)
  105. elif self.use_defaults:
  106. variant = self.default_variant
  107. assert self._norm_variant(variant) == variant, "invalid default variant: %r" % (variant,)
  108. else:
  109. raise TypeError("no variant specified")
  110. self.variant = variant
  111. super(fshp, self).__init__(**kwds)
  112. @classmethod
  113. def _norm_variant(cls, variant):
  114. if isinstance(variant, bytes):
  115. variant = variant.decode("ascii")
  116. if isinstance(variant, unicode):
  117. try:
  118. variant = cls._variant_aliases[variant]
  119. except KeyError:
  120. raise ValueError("invalid fshp variant")
  121. if not isinstance(variant, int):
  122. raise TypeError("fshp variant must be int or known alias")
  123. if variant not in cls._variant_info:
  124. raise ValueError("invalid fshp variant")
  125. return variant
  126. @property
  127. def checksum_alg(self):
  128. return self._variant_info[self.variant][0]
  129. @property
  130. def checksum_size(self):
  131. return self._variant_info[self.variant][1]
  132. #===================================================================
  133. # formatting
  134. #===================================================================
  135. _hash_regex = re.compile(u(r"""
  136. ^
  137. \{FSHP
  138. (\d+)\| # variant
  139. (\d+)\| # salt size
  140. (\d+)\} # rounds
  141. ([a-zA-Z0-9+/]+={0,3}) # digest
  142. $"""), re.X)
  143. @classmethod
  144. def from_string(cls, hash):
  145. hash = to_unicode(hash, "ascii", "hash")
  146. m = cls._hash_regex.match(hash)
  147. if not m:
  148. raise uh.exc.InvalidHashError(cls)
  149. variant, salt_size, rounds, data = m.group(1,2,3,4)
  150. variant = int(variant)
  151. salt_size = int(salt_size)
  152. rounds = int(rounds)
  153. try:
  154. data = b64decode(data.encode("ascii"))
  155. except TypeError:
  156. raise uh.exc.MalformedHashError(cls)
  157. salt = data[:salt_size]
  158. chk = data[salt_size:]
  159. return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant)
  160. def to_string(self):
  161. chk = self.checksum
  162. salt = self.salt
  163. data = bascii_to_str(b64encode(salt+chk))
  164. return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data)
  165. #===================================================================
  166. # backend
  167. #===================================================================
  168. def _calc_checksum(self, secret):
  169. if isinstance(secret, unicode):
  170. secret = secret.encode("utf-8")
  171. # NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed.
  172. # this has only a minimal impact on security,
  173. # but it is worth noting this deviation.
  174. return pbkdf1(
  175. digest=self.checksum_alg,
  176. secret=self.salt,
  177. salt=secret,
  178. rounds=self.rounds,
  179. keylen=self.checksum_size,
  180. )
  181. #===================================================================
  182. # eoc
  183. #===================================================================
  184. #=============================================================================
  185. # eof
  186. #=============================================================================