sun_md5_crypt.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. """passlib.handlers.sun_md5_crypt - Sun's Md5 Crypt, used on Solaris
  2. .. warning::
  3. This implementation may not reproduce
  4. the original Solaris behavior in some border cases.
  5. See documentation for details.
  6. """
  7. #=============================================================================
  8. # imports
  9. #=============================================================================
  10. # core
  11. from hashlib import md5
  12. import re
  13. import logging; log = logging.getLogger(__name__)
  14. from warnings import warn
  15. # site
  16. # pkg
  17. from passlib.utils import to_unicode
  18. from passlib.utils.binary import h64
  19. from passlib.utils.compat import byte_elem_value, irange, u, \
  20. uascii_to_str, unicode, str_to_bascii
  21. import passlib.utils.handlers as uh
  22. # local
  23. __all__ = [
  24. "sun_md5_crypt",
  25. ]
  26. #=============================================================================
  27. # backend
  28. #=============================================================================
  29. # constant data used by alg - Hamlet act 3 scene 1 + null char
  30. # exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt
  31. # from Project Gutenberg.
  32. MAGIC_HAMLET = (
  33. b"To be, or not to be,--that is the question:--\n"
  34. b"Whether 'tis nobler in the mind to suffer\n"
  35. b"The slings and arrows of outrageous fortune\n"
  36. b"Or to take arms against a sea of troubles,\n"
  37. b"And by opposing end them?--To die,--to sleep,--\n"
  38. b"No more; and by a sleep to say we end\n"
  39. b"The heartache, and the thousand natural shocks\n"
  40. b"That flesh is heir to,--'tis a consummation\n"
  41. b"Devoutly to be wish'd. To die,--to sleep;--\n"
  42. b"To sleep! perchance to dream:--ay, there's the rub;\n"
  43. b"For in that sleep of death what dreams may come,\n"
  44. b"When we have shuffled off this mortal coil,\n"
  45. b"Must give us pause: there's the respect\n"
  46. b"That makes calamity of so long life;\n"
  47. b"For who would bear the whips and scorns of time,\n"
  48. b"The oppressor's wrong, the proud man's contumely,\n"
  49. b"The pangs of despis'd love, the law's delay,\n"
  50. b"The insolence of office, and the spurns\n"
  51. b"That patient merit of the unworthy takes,\n"
  52. b"When he himself might his quietus make\n"
  53. b"With a bare bodkin? who would these fardels bear,\n"
  54. b"To grunt and sweat under a weary life,\n"
  55. b"But that the dread of something after death,--\n"
  56. b"The undiscover'd country, from whose bourn\n"
  57. b"No traveller returns,--puzzles the will,\n"
  58. b"And makes us rather bear those ills we have\n"
  59. b"Than fly to others that we know not of?\n"
  60. b"Thus conscience does make cowards of us all;\n"
  61. b"And thus the native hue of resolution\n"
  62. b"Is sicklied o'er with the pale cast of thought;\n"
  63. b"And enterprises of great pith and moment,\n"
  64. b"With this regard, their currents turn awry,\n"
  65. b"And lose the name of action.--Soft you now!\n"
  66. b"The fair Ophelia!--Nymph, in thy orisons\n"
  67. b"Be all my sins remember'd.\n\x00" #<- apparently null at end of C string is included (test vector won't pass otherwise)
  68. )
  69. # NOTE: these sequences are pre-calculated iteration ranges used by X & Y loops w/in rounds function below
  70. xr = irange(7)
  71. _XY_ROUNDS = [
  72. tuple((i,i,i+3) for i in xr), # xrounds 0
  73. tuple((i,i+1,i+4) for i in xr), # xrounds 1
  74. tuple((i,i+8,(i+11)&15) for i in xr), # yrounds 0
  75. tuple((i,(i+9)&15, (i+12)&15) for i in xr), # yrounds 1
  76. ]
  77. del xr
  78. def raw_sun_md5_crypt(secret, rounds, salt):
  79. """given secret & salt, return encoded sun-md5-crypt checksum"""
  80. global MAGIC_HAMLET
  81. assert isinstance(secret, bytes)
  82. assert isinstance(salt, bytes)
  83. # validate rounds
  84. if rounds <= 0:
  85. rounds = 0
  86. real_rounds = 4096 + rounds
  87. # NOTE: spec seems to imply max 'rounds' is 2**32-1
  88. # generate initial digest to start off round 0.
  89. # NOTE: algorithm 'salt' includes full config string w/ trailing "$"
  90. result = md5(secret + salt).digest()
  91. assert len(result) == 16
  92. # NOTE: many things in this function have been inlined (to speed up the loop
  93. # as much as possible), to the point that this code barely resembles
  94. # the algorithm as described in the docs. in particular:
  95. #
  96. # * all accesses to a given bit have been inlined using the formula
  97. # rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1
  98. #
  99. # * the calculation of coinflip value R has been inlined
  100. #
  101. # * the conditional division of coinflip value V has been inlined as
  102. # a shift right of 0 or 1.
  103. #
  104. # * the i, i+3, etc iterations are precalculated in lists.
  105. #
  106. # * the round-based conditional division of x & y is now performed
  107. # by choosing an appropriate precalculated list, so that it only
  108. # calculates the 7 bits which will actually be used.
  109. #
  110. X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS
  111. # NOTE: % appears to be *slightly* slower than &, so we prefer & if possible
  112. round = 0
  113. while round < real_rounds:
  114. # convert last result byte string to list of byte-ints for easy access
  115. rval = [ byte_elem_value(c) for c in result ].__getitem__
  116. # build up X bit by bit
  117. x = 0
  118. xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0
  119. for i, ia, ib in xrounds:
  120. a = rval(ia)
  121. b = rval(ib)
  122. v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
  123. x |= ((rval((v>>3)&15)>>(v&7))&1) << i
  124. # build up Y bit by bit
  125. y = 0
  126. yrounds = Y_ROUNDS_1 if (rval(((round+64)>>3) & 15)>>(round & 7)) & 1 else Y_ROUNDS_0
  127. for i, ia, ib in yrounds:
  128. a = rval(ia)
  129. b = rval(ib)
  130. v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
  131. y |= ((rval((v>>3)&15)>>(v&7))&1) << i
  132. # extract x'th and y'th bit, xoring them together to yeild "coin flip"
  133. coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1
  134. # construct hash for this round
  135. h = md5(result)
  136. if coin:
  137. h.update(MAGIC_HAMLET)
  138. h.update(unicode(round).encode("ascii"))
  139. result = h.digest()
  140. round += 1
  141. # encode output
  142. return h64.encode_transposed_bytes(result, _chk_offsets)
  143. # NOTE: same offsets as md5_crypt
  144. _chk_offsets = (
  145. 12,6,0,
  146. 13,7,1,
  147. 14,8,2,
  148. 15,9,3,
  149. 5,10,4,
  150. 11,
  151. )
  152. #=============================================================================
  153. # handler
  154. #=============================================================================
  155. class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
  156. """This class implements the Sun-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
  157. It supports a variable-length salt, and a variable number of rounds.
  158. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  159. :type salt: str
  160. :param salt:
  161. Optional salt string.
  162. If not specified, a salt will be autogenerated (this is recommended).
  163. If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``.
  164. :type salt_size: int
  165. :param salt_size:
  166. If no salt is specified, this parameter can be used to specify
  167. the size (in characters) of the autogenerated salt.
  168. It currently defaults to 8.
  169. :type rounds: int
  170. :param rounds:
  171. Optional number of rounds to use.
  172. Defaults to 34000, must be between 0 and 4294963199, inclusive.
  173. :type bare_salt: bool
  174. :param bare_salt:
  175. Optional flag used to enable an alternate salt digest behavior
  176. used by some hash strings in this scheme.
  177. This flag can be ignored by most users.
  178. Defaults to ``False``.
  179. (see :ref:`smc-bare-salt` for details).
  180. :type relaxed: bool
  181. :param relaxed:
  182. By default, providing an invalid value for one of the other
  183. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  184. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  185. will be issued instead. Correctable errors include ``rounds``
  186. that are too small or too large, and ``salt`` strings that are too long.
  187. .. versionadded:: 1.6
  188. """
  189. #===================================================================
  190. # class attrs
  191. #===================================================================
  192. name = "sun_md5_crypt"
  193. setting_kwds = ("salt", "rounds", "bare_salt", "salt_size")
  194. checksum_chars = uh.HASH64_CHARS
  195. checksum_size = 22
  196. # NOTE: docs say max password length is 255.
  197. # release 9u2
  198. # NOTE: not sure if original crypt has a salt size limit,
  199. # all instances that have been seen use 8 chars.
  200. default_salt_size = 8
  201. max_salt_size = None
  202. salt_chars = uh.HASH64_CHARS
  203. default_rounds = 34000 # current passlib default
  204. min_rounds = 0
  205. max_rounds = 4294963199 ##2**32-1-4096
  206. # XXX: ^ not sure what it does if past this bound... does 32 int roll over?
  207. rounds_cost = "linear"
  208. ident_values = (u("$md5$"), u("$md5,"))
  209. #===================================================================
  210. # instance attrs
  211. #===================================================================
  212. bare_salt = False # flag to indicate legacy hashes that lack "$$" suffix
  213. #===================================================================
  214. # constructor
  215. #===================================================================
  216. def __init__(self, bare_salt=False, **kwds):
  217. self.bare_salt = bare_salt
  218. super(sun_md5_crypt, self).__init__(**kwds)
  219. #===================================================================
  220. # internal helpers
  221. #===================================================================
  222. @classmethod
  223. def identify(cls, hash):
  224. hash = uh.to_unicode_for_identify(hash)
  225. return hash.startswith(cls.ident_values)
  226. @classmethod
  227. def from_string(cls, hash):
  228. hash = to_unicode(hash, "ascii", "hash")
  229. #
  230. # detect if hash specifies rounds value.
  231. # if so, parse and validate it.
  232. # by end, set 'rounds' to int value, and 'tail' containing salt+chk
  233. #
  234. if hash.startswith(u("$md5$")):
  235. rounds = 0
  236. salt_idx = 5
  237. elif hash.startswith(u("$md5,rounds=")):
  238. idx = hash.find(u("$"), 12)
  239. if idx == -1:
  240. raise uh.exc.MalformedHashError(cls, "unexpected end of rounds")
  241. rstr = hash[12:idx]
  242. try:
  243. rounds = int(rstr)
  244. except ValueError:
  245. raise uh.exc.MalformedHashError(cls, "bad rounds")
  246. if rstr != unicode(rounds):
  247. raise uh.exc.ZeroPaddedRoundsError(cls)
  248. if rounds == 0:
  249. # NOTE: not sure if this is forbidden by spec or not;
  250. # but allowing it would complicate things,
  251. # and it should never occur anyways.
  252. raise uh.exc.MalformedHashError(cls, "explicit zero rounds")
  253. salt_idx = idx+1
  254. else:
  255. raise uh.exc.InvalidHashError(cls)
  256. #
  257. # salt/checksum separation is kinda weird,
  258. # to deal cleanly with some backward-compatible workarounds
  259. # implemented by original implementation.
  260. #
  261. chk_idx = hash.rfind(u("$"), salt_idx)
  262. if chk_idx == -1:
  263. # ''-config for $-hash
  264. salt = hash[salt_idx:]
  265. chk = None
  266. bare_salt = True
  267. elif chk_idx == len(hash)-1:
  268. if chk_idx > salt_idx and hash[-2] == u("$"):
  269. raise uh.exc.MalformedHashError(cls, "too many '$' separators")
  270. # $-config for $$-hash
  271. salt = hash[salt_idx:-1]
  272. chk = None
  273. bare_salt = False
  274. elif chk_idx > 0 and hash[chk_idx-1] == u("$"):
  275. # $$-hash
  276. salt = hash[salt_idx:chk_idx-1]
  277. chk = hash[chk_idx+1:]
  278. bare_salt = False
  279. else:
  280. # $-hash
  281. salt = hash[salt_idx:chk_idx]
  282. chk = hash[chk_idx+1:]
  283. bare_salt = True
  284. return cls(
  285. rounds=rounds,
  286. salt=salt,
  287. checksum=chk,
  288. bare_salt=bare_salt,
  289. )
  290. def to_string(self, _withchk=True):
  291. ss = u('') if self.bare_salt else u('$')
  292. rounds = self.rounds
  293. if rounds > 0:
  294. hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss)
  295. else:
  296. hash = u("$md5$%s%s") % (self.salt, ss)
  297. if _withchk:
  298. chk = self.checksum
  299. hash = u("%s$%s") % (hash, chk)
  300. return uascii_to_str(hash)
  301. #===================================================================
  302. # primary interface
  303. #===================================================================
  304. # TODO: if we're on solaris, check for native crypt() support.
  305. # this will require extra testing, to make sure native crypt
  306. # actually behaves correctly. of particular importance:
  307. # when using ""-config, make sure to append "$x" to string.
  308. def _calc_checksum(self, secret):
  309. # NOTE: no reference for how sun_md5_crypt handles unicode
  310. if isinstance(secret, unicode):
  311. secret = secret.encode("utf-8")
  312. config = str_to_bascii(self.to_string(_withchk=False))
  313. return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii")
  314. #===================================================================
  315. # eoc
  316. #===================================================================
  317. #=============================================================================
  318. # eof
  319. #=============================================================================