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.
 
 
 
 

347 lines
13 KiB

  1. """passlib.handlers.md5_crypt - md5-crypt algorithm"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. # core
  6. from hashlib import md5
  7. import logging; log = logging.getLogger(__name__)
  8. # site
  9. # pkg
  10. from passlib.utils import safe_crypt, test_crypt, repeat_string
  11. from passlib.utils.binary import h64
  12. from passlib.utils.compat import unicode, u
  13. import passlib.utils.handlers as uh
  14. # local
  15. __all__ = [
  16. "md5_crypt",
  17. "apr_md5_crypt",
  18. ]
  19. #=============================================================================
  20. # pure-python backend
  21. #=============================================================================
  22. _BNULL = b"\x00"
  23. _MD5_MAGIC = b"$1$"
  24. _APR_MAGIC = b"$apr1$"
  25. # pre-calculated offsets used to speed up C digest stage (see notes below).
  26. # sequence generated using the following:
  27. ##perms_order = "p,pp,ps,psp,sp,spp".split(",")
  28. ##def offset(i):
  29. ## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") +
  30. ## ("p" if i % 7 else "") + ("" if i % 2 else "p"))
  31. ## return perms_order.index(key)
  32. ##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)]
  33. _c_digest_offsets = (
  34. (0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3),
  35. (4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1),
  36. (4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3),
  37. )
  38. # map used to transpose bytes when encoding final digest
  39. _transpose_map = (12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11)
  40. def _raw_md5_crypt(pwd, salt, use_apr=False):
  41. """perform raw md5-crypt calculation
  42. this function provides a pure-python implementation of the internals
  43. for the MD5-Crypt algorithms; it doesn't handle any of the
  44. parsing/validation of the hash strings themselves.
  45. :arg pwd: password chars/bytes to hash
  46. :arg salt: salt chars to use
  47. :arg use_apr: use apache variant
  48. :returns:
  49. encoded checksum chars
  50. """
  51. # NOTE: regarding 'apr' format:
  52. # really, apache? you had to invent a whole new "$apr1$" format,
  53. # when all you did was change the ident incorporated into the hash?
  54. # would love to find webpage explaining why just using a portable
  55. # implementation of $1$ wasn't sufficient. *nothing else* was changed.
  56. #===================================================================
  57. # init & validate inputs
  58. #===================================================================
  59. # validate secret
  60. # XXX: not sure what official unicode policy is, using this as default
  61. if isinstance(pwd, unicode):
  62. pwd = pwd.encode("utf-8")
  63. assert isinstance(pwd, bytes), "pwd not unicode or bytes"
  64. if _BNULL in pwd:
  65. raise uh.exc.NullPasswordError(md5_crypt)
  66. pwd_len = len(pwd)
  67. # validate salt - should have been taken care of by caller
  68. assert isinstance(salt, unicode), "salt not unicode"
  69. salt = salt.encode("ascii")
  70. assert len(salt) < 9, "salt too large"
  71. # NOTE: spec says salts larger than 8 bytes should be truncated,
  72. # instead of causing an error. this function assumes that's been
  73. # taken care of by the handler class.
  74. # load APR specific constants
  75. if use_apr:
  76. magic = _APR_MAGIC
  77. else:
  78. magic = _MD5_MAGIC
  79. #===================================================================
  80. # digest B - used as subinput to digest A
  81. #===================================================================
  82. db = md5(pwd + salt + pwd).digest()
  83. #===================================================================
  84. # digest A - used to initialize first round of digest C
  85. #===================================================================
  86. # start out with pwd + magic + salt
  87. a_ctx = md5(pwd + magic + salt)
  88. a_ctx_update = a_ctx.update
  89. # add pwd_len bytes of b, repeating b as many times as needed.
  90. a_ctx_update(repeat_string(db, pwd_len))
  91. # add null chars & first char of password
  92. # NOTE: this may have historically been a bug,
  93. # where they meant to use db[0] instead of B_NULL,
  94. # but the original code memclear'ed db,
  95. # and now all implementations have to use this.
  96. i = pwd_len
  97. evenchar = pwd[:1]
  98. while i:
  99. a_ctx_update(_BNULL if i & 1 else evenchar)
  100. i >>= 1
  101. # finish A
  102. da = a_ctx.digest()
  103. #===================================================================
  104. # digest C - for a 1000 rounds, combine A, S, and P
  105. # digests in various ways; in order to burn CPU time.
  106. #===================================================================
  107. # NOTE: the original MD5-Crypt implementation performs the C digest
  108. # calculation using the following loop:
  109. #
  110. ##dc = da
  111. ##i = 0
  112. ##while i < rounds:
  113. ## tmp_ctx = md5(pwd if i & 1 else dc)
  114. ## if i % 3:
  115. ## tmp_ctx.update(salt)
  116. ## if i % 7:
  117. ## tmp_ctx.update(pwd)
  118. ## tmp_ctx.update(dc if i & 1 else pwd)
  119. ## dc = tmp_ctx.digest()
  120. ## i += 1
  121. #
  122. # The code Passlib uses (below) implements an equivalent algorithm,
  123. # it's just been heavily optimized to pre-calculate a large number
  124. # of things beforehand. It works off of a couple of observations
  125. # about the original algorithm:
  126. #
  127. # 1. each round is a combination of 'dc', 'salt', and 'pwd'; and the exact
  128. # combination is determined by whether 'i' a multiple of 2,3, and/or 7.
  129. # 2. since lcm(2,3,7)==42, the series of combinations will repeat
  130. # every 42 rounds.
  131. # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
  132. # while odd rounds 1-41 consist of hash(round-specific-constant + dc)
  133. #
  134. # Using these observations, the following code...
  135. # * calculates the round-specific combination of salt & pwd for each round 0-41
  136. # * runs through as many 42-round blocks as possible (23)
  137. # * runs through as many pairs of rounds as needed for remaining rounds (17)
  138. # * this results in the required 42*23+2*17=1000 rounds required by md5_crypt.
  139. #
  140. # this cuts out a lot of the control overhead incurred when running the
  141. # original loop 1000 times in python, resulting in ~20% increase in
  142. # speed under CPython (though still 2x slower than glibc crypt)
  143. # prepare the 6 combinations of pwd & salt which are needed
  144. # (order of 'perms' must match how _c_digest_offsets was generated)
  145. pwd_pwd = pwd+pwd
  146. pwd_salt = pwd+salt
  147. perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd]
  148. # build up list of even-round & odd-round constants,
  149. # and store in 21-element list as (even,odd) pairs.
  150. data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]
  151. # perform 23 blocks of 42 rounds each (for a total of 966 rounds)
  152. dc = da
  153. blocks = 23
  154. while blocks:
  155. for even, odd in data:
  156. dc = md5(odd + md5(dc + even).digest()).digest()
  157. blocks -= 1
  158. # perform 17 more pairs of rounds (34 more rounds, for a total of 1000)
  159. for even, odd in data[:17]:
  160. dc = md5(odd + md5(dc + even).digest()).digest()
  161. #===================================================================
  162. # encode digest using appropriate transpose map
  163. #===================================================================
  164. return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii")
  165. #=============================================================================
  166. # handler
  167. #=============================================================================
  168. class _MD5_Common(uh.HasSalt, uh.GenericHandler):
  169. """common code for md5_crypt and apr_md5_crypt"""
  170. #===================================================================
  171. # class attrs
  172. #===================================================================
  173. # name - set in subclass
  174. setting_kwds = ("salt", "salt_size")
  175. # ident - set in subclass
  176. checksum_size = 22
  177. checksum_chars = uh.HASH64_CHARS
  178. max_salt_size = 8
  179. salt_chars = uh.HASH64_CHARS
  180. #===================================================================
  181. # methods
  182. #===================================================================
  183. @classmethod
  184. def from_string(cls, hash):
  185. salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
  186. return cls(salt=salt, checksum=chk)
  187. def to_string(self):
  188. return uh.render_mc2(self.ident, self.salt, self.checksum)
  189. # _calc_checksum() - provided by subclass
  190. #===================================================================
  191. # eoc
  192. #===================================================================
  193. class md5_crypt(uh.HasManyBackends, _MD5_Common):
  194. """This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
  195. It supports a variable-length salt.
  196. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  197. :type salt: str
  198. :param salt:
  199. Optional salt string.
  200. If not specified, one will be autogenerated (this is recommended).
  201. If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
  202. :type salt_size: int
  203. :param salt_size:
  204. Optional number of characters to use when autogenerating new salts.
  205. Defaults to 8, but can be any value between 0 and 8.
  206. (This is mainly needed when generating Cisco-compatible hashes,
  207. which require ``salt_size=4``).
  208. :type relaxed: bool
  209. :param relaxed:
  210. By default, providing an invalid value for one of the other
  211. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  212. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  213. will be issued instead. Correctable errors include
  214. ``salt`` strings that are too long.
  215. .. versionadded:: 1.6
  216. """
  217. #===================================================================
  218. # class attrs
  219. #===================================================================
  220. name = "md5_crypt"
  221. ident = u("$1$")
  222. #===================================================================
  223. # methods
  224. #===================================================================
  225. # FIXME: can't find definitive policy on how md5-crypt handles non-ascii.
  226. # all backends currently coerce -> utf-8
  227. backends = ("os_crypt", "builtin")
  228. #---------------------------------------------------------------
  229. # os_crypt backend
  230. #---------------------------------------------------------------
  231. @classmethod
  232. def _load_backend_os_crypt(cls):
  233. if test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/'):
  234. cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
  235. return True
  236. else:
  237. return False
  238. def _calc_checksum_os_crypt(self, secret):
  239. config = self.ident + self.salt
  240. hash = safe_crypt(secret, config)
  241. if hash:
  242. assert hash.startswith(config) and len(hash) == len(config) + 23
  243. return hash[-22:]
  244. else:
  245. # py3's crypt.crypt() can't handle non-utf8 bytes.
  246. # fallback to builtin alg, which is always available.
  247. return self._calc_checksum_builtin(secret)
  248. #---------------------------------------------------------------
  249. # builtin backend
  250. #---------------------------------------------------------------
  251. @classmethod
  252. def _load_backend_builtin(cls):
  253. cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
  254. return True
  255. def _calc_checksum_builtin(self, secret):
  256. return _raw_md5_crypt(secret, self.salt)
  257. #===================================================================
  258. # eoc
  259. #===================================================================
  260. class apr_md5_crypt(_MD5_Common):
  261. """This class implements the Apr-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
  262. It supports a variable-length salt.
  263. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  264. :type salt: str
  265. :param salt:
  266. Optional salt string.
  267. If not specified, one will be autogenerated (this is recommended).
  268. If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
  269. :type relaxed: bool
  270. :param relaxed:
  271. By default, providing an invalid value for one of the other
  272. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  273. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  274. will be issued instead. Correctable errors include
  275. ``salt`` strings that are too long.
  276. .. versionadded:: 1.6
  277. """
  278. #===================================================================
  279. # class attrs
  280. #===================================================================
  281. name = "apr_md5_crypt"
  282. ident = u("$apr1$")
  283. #===================================================================
  284. # methods
  285. #===================================================================
  286. def _calc_checksum(self, secret):
  287. return _raw_md5_crypt(secret, self.salt, use_apr=True)
  288. #===================================================================
  289. # eoc
  290. #===================================================================
  291. #=============================================================================
  292. # eof
  293. #=============================================================================