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.
 
 
 
 

354 rivejä
14 KiB

  1. """passlib.ifc - abstract interfaces used by Passlib"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. # core
  6. import logging; log = logging.getLogger(__name__)
  7. import sys
  8. # site
  9. # pkg
  10. from passlib.utils.decor import deprecated_method
  11. # local
  12. __all__ = [
  13. "PasswordHash",
  14. ]
  15. #=============================================================================
  16. # 2/3 compatibility helpers
  17. #=============================================================================
  18. def recreate_with_metaclass(meta):
  19. """class decorator that re-creates class using metaclass"""
  20. def builder(cls):
  21. if meta is type(cls):
  22. return cls
  23. return meta(cls.__name__, cls.__bases__, cls.__dict__.copy())
  24. return builder
  25. #=============================================================================
  26. # PasswordHash interface
  27. #=============================================================================
  28. from abc import ABCMeta, abstractmethod, abstractproperty
  29. # TODO: make this actually use abstractproperty(),
  30. # now that we dropped py25, 'abc' is always available.
  31. # XXX: rename to PasswordHasher?
  32. @recreate_with_metaclass(ABCMeta)
  33. class PasswordHash(object):
  34. """This class describes an abstract interface which all password hashes
  35. in Passlib adhere to. Under Python 2.6 and up, this is an actual
  36. Abstract Base Class built using the :mod:`!abc` module.
  37. See the Passlib docs for full documentation.
  38. """
  39. #===================================================================
  40. # class attributes
  41. #===================================================================
  42. #---------------------------------------------------------------
  43. # general information
  44. #---------------------------------------------------------------
  45. ##name
  46. ##setting_kwds
  47. ##context_kwds
  48. #: flag which indicates this hasher matches a "disabled" hash
  49. #: (e.g. unix_disabled, or django_disabled); and doesn't actually
  50. #: depend on the provided password.
  51. is_disabled = False
  52. #: Should be None, or a positive integer indicating hash
  53. #: doesn't support secrets larger than this value.
  54. #: Whether hash throws error or silently truncates secret
  55. #: depends on .truncate_error and .truncate_verify_reject flags below.
  56. #: NOTE: calls may treat as boolean, since value will never be 0.
  57. #: .. versionadded:: 1.7
  58. #: .. TODO: passlib 1.8: deprecate/rename this attr to "max_secret_size"?
  59. truncate_size = None
  60. # NOTE: these next two default to the optimistic "ideal",
  61. # most hashes in passlib have to default to False
  62. # for backward compat and/or expected behavior with existing hashes.
  63. #: If True, .hash() should throw a :exc:`~passlib.exc.PasswordSizeError` for
  64. #: any secrets larger than .truncate_size. Many hashers default to False
  65. #: for historical / compatibility purposes, indicating they will silently
  66. #: truncate instead. All such hashers SHOULD support changing
  67. #: the policy via ``.using(truncate_error=True)``.
  68. #: .. versionadded:: 1.7
  69. #: .. TODO: passlib 1.8: deprecate/rename this attr to "truncate_hash_error"?
  70. truncate_error = True
  71. #: If True, .verify() should reject secrets larger than max_password_size.
  72. #: Many hashers default to False for historical / compatibility purposes,
  73. #: indicating they will match on the truncated portion instead.
  74. #: .. versionadded:: 1.7.1
  75. truncate_verify_reject = True
  76. #---------------------------------------------------------------
  77. # salt information -- if 'salt' in setting_kwds
  78. #---------------------------------------------------------------
  79. ##min_salt_size
  80. ##max_salt_size
  81. ##default_salt_size
  82. ##salt_chars
  83. ##default_salt_chars
  84. #---------------------------------------------------------------
  85. # rounds information -- if 'rounds' in setting_kwds
  86. #---------------------------------------------------------------
  87. ##min_rounds
  88. ##max_rounds
  89. ##default_rounds
  90. ##rounds_cost
  91. #---------------------------------------------------------------
  92. # encoding info -- if 'encoding' in context_kwds
  93. #---------------------------------------------------------------
  94. ##default_encoding
  95. #===================================================================
  96. # primary methods
  97. #===================================================================
  98. @classmethod
  99. @abstractmethod
  100. def hash(cls, secret, # *
  101. **setting_and_context_kwds): # pragma: no cover -- abstract method
  102. r"""
  103. Hash secret, returning result.
  104. Should handle generating salt, etc, and should return string
  105. containing identifier, salt & other configuration, as well as digest.
  106. :param \\*\\*settings_kwds:
  107. Pass in settings to customize configuration of resulting hash.
  108. .. deprecated:: 1.7
  109. Starting with Passlib 1.7, callers should no longer pass settings keywords
  110. (e.g. ``rounds`` or ``salt`` directly to :meth:`!hash`); should use
  111. ``.using(**settings).hash(secret)`` construction instead.
  112. Support will be removed in Passlib 2.0.
  113. :param \\*\\*context_kwds:
  114. Specific algorithms may require context-specific information (such as the user login).
  115. """
  116. # FIXME: need stub for classes that define .encrypt() instead ...
  117. # this should call .encrypt(), and check for recursion back to here.
  118. raise NotImplementedError("must be implemented by subclass")
  119. @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()")
  120. @classmethod
  121. def encrypt(cls, *args, **kwds):
  122. """
  123. Legacy alias for :meth:`hash`.
  124. .. deprecated:: 1.7
  125. This method was renamed to :meth:`!hash` in version 1.7.
  126. This alias will be removed in version 2.0, and should only
  127. be used for compatibility with Passlib 1.3 - 1.6.
  128. """
  129. return cls.hash(*args, **kwds)
  130. # XXX: could provide default implementation which hands value to
  131. # hash(), and then does constant-time comparision on the result
  132. # (after making both are same string type)
  133. @classmethod
  134. @abstractmethod
  135. def verify(cls, secret, hash, **context_kwds): # pragma: no cover -- abstract method
  136. """verify secret against hash, returns True/False"""
  137. raise NotImplementedError("must be implemented by subclass")
  138. #===================================================================
  139. # configuration
  140. #===================================================================
  141. @classmethod
  142. @abstractmethod
  143. def using(cls, relaxed=False, **kwds):
  144. """
  145. Return another hasher object (typically a subclass of the current one),
  146. which integrates the configuration options specified by ``kwds``.
  147. This should *always* return a new object, even if no configuration options are changed.
  148. .. todo::
  149. document which options are accepted.
  150. :returns:
  151. typically returns a subclass for most hasher implementations.
  152. .. todo::
  153. add this method to main documentation.
  154. """
  155. raise NotImplementedError("must be implemented by subclass")
  156. #===================================================================
  157. # migration
  158. #===================================================================
  159. @classmethod
  160. def needs_update(cls, hash, secret=None):
  161. """
  162. check if hash's configuration is outside desired bounds,
  163. or contains some other internal option which requires
  164. updating the password hash.
  165. :param hash:
  166. hash string to examine
  167. :param secret:
  168. optional secret known to have verified against the provided hash.
  169. (this is used by some hashes to detect legacy algorithm mistakes).
  170. :return:
  171. whether secret needs re-hashing.
  172. .. versionadded:: 1.7
  173. """
  174. # by default, always report that we don't need update
  175. return False
  176. #===================================================================
  177. # additional methods
  178. #===================================================================
  179. @classmethod
  180. @abstractmethod
  181. def identify(cls, hash): # pragma: no cover -- abstract method
  182. """check if hash belongs to this scheme, returns True/False"""
  183. raise NotImplementedError("must be implemented by subclass")
  184. @deprecated_method(deprecated="1.7", removed="2.0")
  185. @classmethod
  186. def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method
  187. """
  188. compile settings into a configuration string for genhash()
  189. .. deprecated:: 1.7
  190. As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0.
  191. For all known real-world uses, hashing a constant string
  192. should provide equivalent functionality.
  193. This deprecation may be reversed if a use-case presents itself in the mean time.
  194. """
  195. # NOTE: this fallback runs full hash alg, w/ whatever cost param is passed along.
  196. # implementations (esp ones w/ variable cost) will want to subclass this
  197. # with a constant-time implementation that just renders a config string.
  198. if cls.context_kwds:
  199. raise NotImplementedError("must be implemented by subclass")
  200. return cls.using(**setting_kwds).hash("")
  201. @deprecated_method(deprecated="1.7", removed="2.0")
  202. @classmethod
  203. def genhash(cls, secret, config, **context):
  204. """
  205. generated hash for secret, using settings from config/hash string
  206. .. deprecated:: 1.7
  207. As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0.
  208. This deprecation may be reversed if a use-case presents itself in the mean time.
  209. """
  210. # XXX: if hashes reliably offered a .parse() method, could make a fallback for this.
  211. raise NotImplementedError("must be implemented by subclass")
  212. #===================================================================
  213. # undocumented methods / attributes
  214. #===================================================================
  215. # the following entry points are used internally by passlib,
  216. # and aren't documented as part of the exposed interface.
  217. # they are subject to change between releases,
  218. # but are documented here so there's a list of them *somewhere*.
  219. #---------------------------------------------------------------
  220. # extra metdata
  221. #---------------------------------------------------------------
  222. #: this attribute shouldn't be used by hashers themselves,
  223. #: it's reserved for the CryptContext to track which hashers are deprecated.
  224. #: Note the context will only set this on objects it owns (and generated by .using()),
  225. #: and WONT set it on global objects.
  226. #: [added in 1.7]
  227. #: TODO: document this, or at least the use of testing for
  228. #: 'CryptContext().handler().deprecated'
  229. deprecated = False
  230. #: optionally present if hasher corresponds to format built into Django.
  231. #: this attribute (if not None) should be the Django 'algorithm' name.
  232. #: also indicates to passlib.ext.django that (when installed in django),
  233. #: django's native hasher should be used in preference to this one.
  234. ## django_name
  235. #---------------------------------------------------------------
  236. # checksum information - defined for many hashes
  237. #---------------------------------------------------------------
  238. ## checksum_chars
  239. ## checksum_size
  240. #---------------------------------------------------------------
  241. # experimental methods
  242. #---------------------------------------------------------------
  243. ##@classmethod
  244. ##def normhash(cls, hash):
  245. ## """helper to clean up non-canonic instances of hash.
  246. ## currently only provided by bcrypt() to fix an historical passlib issue.
  247. ## """
  248. # experimental helper to parse hash into components.
  249. ##@classmethod
  250. ##def parsehash(cls, hash, checksum=True, sanitize=False):
  251. ## """helper to parse hash into components, returns dict"""
  252. # experiment helper to estimate bitsize of different hashes,
  253. # implement for GenericHandler, but may be currently be off for some hashes.
  254. # want to expand this into a way to programmatically compare
  255. # "strengths" of different hashes and hash algorithms.
  256. # still needs to have some factor for estimate relative cost per round,
  257. # ala in the style of the scrypt whitepaper.
  258. ##@classmethod
  259. ##def bitsize(cls, **kwds):
  260. ## """returns dict mapping component -> bits contributed.
  261. ## components currently include checksum, salt, rounds.
  262. ## """
  263. #===================================================================
  264. # eoc
  265. #===================================================================
  266. class DisabledHash(PasswordHash):
  267. """
  268. extended disabled-hash methods; only need be present if .disabled = True
  269. """
  270. is_disabled = True
  271. @classmethod
  272. def disable(cls, hash=None):
  273. """
  274. return string representing a 'disabled' hash;
  275. optionally including previously enabled hash
  276. (this is up to the individual scheme).
  277. """
  278. # default behavior: ignore original hash, return standalone marker
  279. return cls.hash("")
  280. @classmethod
  281. def enable(cls, hash):
  282. """
  283. given a disabled-hash string,
  284. extract previously-enabled hash if one is present,
  285. otherwise raises ValueError
  286. """
  287. # default behavior: no way to restore original hash
  288. raise ValueError("cannot restore original hash")
  289. #=============================================================================
  290. # eof
  291. #=============================================================================