codekingpro/portable-devtools
114k
1"""passlib.handlers.sun_md5_crypt - Sun's Md5 Crypt, used on Solaris2 3.. warning::4 5 This implementation may not reproduce6 the original Solaris behavior in some border cases.7 See documentation for details.8"""9 10#=============================================================================11# imports12#=============================================================================13# core14from hashlib import md515import re16import logging; log = logging.getLogger(__name__)17from warnings import warn18# site19# pkg20from passlib.utils import to_unicode21from passlib.utils.binary import h6422from passlib.utils.compat import byte_elem_value, irange, u, \23 uascii_to_str, unicode, str_to_bascii24import passlib.utils.handlers as uh25# local26__all__ = [27 "sun_md5_crypt",28]29 30#=============================================================================31# backend32#=============================================================================33# constant data used by alg - Hamlet act 3 scene 1 + null char34# exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt35# from Project Gutenberg.36 37MAGIC_HAMLET = (38 b"To be, or not to be,--that is the question:--\n"39 b"Whether 'tis nobler in the mind to suffer\n"40 b"The slings and arrows of outrageous fortune\n"41 b"Or to take arms against a sea of troubles,\n"42 b"And by opposing end them?--To die,--to sleep,--\n"43 b"No more; and by a sleep to say we end\n"44 b"The heartache, and the thousand natural shocks\n"45 b"That flesh is heir to,--'tis a consummation\n"46 b"Devoutly to be wish'd. To die,--to sleep;--\n"47 b"To sleep! perchance to dream:--ay, there's the rub;\n"48 b"For in that sleep of death what dreams may come,\n"49 b"When we have shuffled off this mortal coil,\n"50 b"Must give us pause: there's the respect\n"51 b"That makes calamity of so long life;\n"52 b"For who would bear the whips and scorns of time,\n"53 b"The oppressor's wrong, the proud man's contumely,\n"54 b"The pangs of despis'd love, the law's delay,\n"55 b"The insolence of office, and the spurns\n"56 b"That patient merit of the unworthy takes,\n"57 b"When he himself might his quietus make\n"58 b"With a bare bodkin? who would these fardels bear,\n"59 b"To grunt and sweat under a weary life,\n"60 b"But that the dread of something after death,--\n"61 b"The undiscover'd country, from whose bourn\n"62 b"No traveller returns,--puzzles the will,\n"63 b"And makes us rather bear those ills we have\n"64 b"Than fly to others that we know not of?\n"65 b"Thus conscience does make cowards of us all;\n"66 b"And thus the native hue of resolution\n"67 b"Is sicklied o'er with the pale cast of thought;\n"68 b"And enterprises of great pith and moment,\n"69 b"With this regard, their currents turn awry,\n"70 b"And lose the name of action.--Soft you now!\n"71 b"The fair Ophelia!--Nymph, in thy orisons\n"72 b"Be all my sins remember'd.\n\x00" #<- apparently null at end of C string is included (test vector won't pass otherwise)73)74 75# NOTE: these sequences are pre-calculated iteration ranges used by X & Y loops w/in rounds function below76xr = irange(7)77_XY_ROUNDS = [78 tuple((i,i,i+3) for i in xr), # xrounds 079 tuple((i,i+1,i+4) for i in xr), # xrounds 180 tuple((i,i+8,(i+11)&15) for i in xr), # yrounds 081 tuple((i,(i+9)&15, (i+12)&15) for i in xr), # yrounds 182]83del xr84 85def raw_sun_md5_crypt(secret, rounds, salt):86 """given secret & salt, return encoded sun-md5-crypt checksum"""87 global MAGIC_HAMLET88 assert isinstance(secret, bytes)89 assert isinstance(salt, bytes)90 91 # validate rounds92 if rounds <= 0:93 rounds = 094 real_rounds = 4096 + rounds95 # NOTE: spec seems to imply max 'rounds' is 2**32-196 97 # generate initial digest to start off round 0.98 # NOTE: algorithm 'salt' includes full config string w/ trailing "$"99 result = md5(secret + salt).digest()100 assert len(result) == 16101 102 # NOTE: many things in this function have been inlined (to speed up the loop103 # as much as possible), to the point that this code barely resembles104 # the algorithm as described in the docs. in particular:105 #106 # * all accesses to a given bit have been inlined using the formula107 # rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1108 #109 # * the calculation of coinflip value R has been inlined110 #111 # * the conditional division of coinflip value V has been inlined as112 # a shift right of 0 or 1.113 #114 # * the i, i+3, etc iterations are precalculated in lists.115 #116 # * the round-based conditional division of x & y is now performed117 # by choosing an appropriate precalculated list, so that it only118 # calculates the 7 bits which will actually be used.119 #120 X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS121 122 # NOTE: % appears to be *slightly* slower than &, so we prefer & if possible123 124 round = 0125 while round < real_rounds:126 # convert last result byte string to list of byte-ints for easy access127 rval = [ byte_elem_value(c) for c in result ].__getitem__128 129 # build up X bit by bit130 x = 0131 xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0132 for i, ia, ib in xrounds:133 a = rval(ia)134 b = rval(ib)135 v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)136 x |= ((rval((v>>3)&15)>>(v&7))&1) << i137 138 # build up Y bit by bit139 y = 0140 yrounds = Y_ROUNDS_1 if (rval(((round+64)>>3) & 15)>>(round & 7)) & 1 else Y_ROUNDS_0141 for i, ia, ib in yrounds:142 a = rval(ia)143 b = rval(ib)144 v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)145 y |= ((rval((v>>3)&15)>>(v&7))&1) << i146 147 # extract x'th and y'th bit, xoring them together to yeild "coin flip"148 coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1149 150 # construct hash for this round151 h = md5(result)152 if coin:153 h.update(MAGIC_HAMLET)154 h.update(unicode(round).encode("ascii"))155 result = h.digest()156 157 round += 1158 159 # encode output160 return h64.encode_transposed_bytes(result, _chk_offsets)161 162# NOTE: same offsets as md5_crypt163_chk_offsets = (164 12,6,0,165 13,7,1,166 14,8,2,167 15,9,3,168 5,10,4,169 11,170)171 172#=============================================================================173# handler174#=============================================================================175class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):176 """This class implements the Sun-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.177 178 It supports a variable-length salt, and a variable number of rounds.179 180 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:181 182 :type salt: str183 :param salt:184 Optional salt string.185 If not specified, a salt will be autogenerated (this is recommended).186 If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``.187 188 :type salt_size: int189 :param salt_size:190 If no salt is specified, this parameter can be used to specify191 the size (in characters) of the autogenerated salt.192 It currently defaults to 8.193 194 :type rounds: int195 :param rounds:196 Optional number of rounds to use.197 Defaults to 34000, must be between 0 and 4294963199, inclusive.198 199 :type bare_salt: bool200 :param bare_salt:201 Optional flag used to enable an alternate salt digest behavior202 used by some hash strings in this scheme.203 This flag can be ignored by most users.204 Defaults to ``False``.205 (see :ref:`smc-bare-salt` for details).206 207 :type relaxed: bool208 :param relaxed:209 By default, providing an invalid value for one of the other210 keywords will result in a :exc:`ValueError`. If ``relaxed=True``,211 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`212 will be issued instead. Correctable errors include ``rounds``213 that are too small or too large, and ``salt`` strings that are too long.214 215 .. versionadded:: 1.6216 """217 #===================================================================218 # class attrs219 #===================================================================220 name = "sun_md5_crypt"221 setting_kwds = ("salt", "rounds", "bare_salt", "salt_size")222 checksum_chars = uh.HASH64_CHARS223 checksum_size = 22224 225 # NOTE: docs say max password length is 255.226 # release 9u2227 228 # NOTE: not sure if original crypt has a salt size limit,229 # all instances that have been seen use 8 chars.230 default_salt_size = 8231 max_salt_size = None232 salt_chars = uh.HASH64_CHARS233 234 default_rounds = 34000 # current passlib default235 min_rounds = 0236 max_rounds = 4294963199 ##2**32-1-4096237 # XXX: ^ not sure what it does if past this bound... does 32 int roll over?238 rounds_cost = "linear"239 240 ident_values = (u("$md5$"), u("$md5,"))241 242 #===================================================================243 # instance attrs244 #===================================================================245 bare_salt = False # flag to indicate legacy hashes that lack "$$" suffix246 247 #===================================================================248 # constructor249 #===================================================================250 def __init__(self, bare_salt=False, **kwds):251 self.bare_salt = bare_salt252 super(sun_md5_crypt, self).__init__(**kwds)253 254 #===================================================================255 # internal helpers256 #===================================================================257 @classmethod258 def identify(cls, hash):259 hash = uh.to_unicode_for_identify(hash)260 return hash.startswith(cls.ident_values)261 262 @classmethod263 def from_string(cls, hash):264 hash = to_unicode(hash, "ascii", "hash")265 266 #267 # detect if hash specifies rounds value.268 # if so, parse and validate it.269 # by end, set 'rounds' to int value, and 'tail' containing salt+chk270 #271 if hash.startswith(u("$md5$")):272 rounds = 0273 salt_idx = 5274 elif hash.startswith(u("$md5,rounds=")):275 idx = hash.find(u("$"), 12)276 if idx == -1:277 raise uh.exc.MalformedHashError(cls, "unexpected end of rounds")278 rstr = hash[12:idx]279 try:280 rounds = int(rstr)281 except ValueError:282 raise uh.exc.MalformedHashError(cls, "bad rounds")283 if rstr != unicode(rounds):284 raise uh.exc.ZeroPaddedRoundsError(cls)285 if rounds == 0:286 # NOTE: not sure if this is forbidden by spec or not;287 # but allowing it would complicate things,288 # and it should never occur anyways.289 raise uh.exc.MalformedHashError(cls, "explicit zero rounds")290 salt_idx = idx+1291 else:292 raise uh.exc.InvalidHashError(cls)293 294 #295 # salt/checksum separation is kinda weird,296 # to deal cleanly with some backward-compatible workarounds297 # implemented by original implementation.298 #299 chk_idx = hash.rfind(u("$"), salt_idx)300 if chk_idx == -1:301 # ''-config for $-hash302 salt = hash[salt_idx:]303 chk = None304 bare_salt = True305 elif chk_idx == len(hash)-1:306 if chk_idx > salt_idx and hash[-2] == u("$"):307 raise uh.exc.MalformedHashError(cls, "too many '$' separators")308 # $-config for $$-hash309 salt = hash[salt_idx:-1]310 chk = None311 bare_salt = False312 elif chk_idx > 0 and hash[chk_idx-1] == u("$"):313 # $$-hash314 salt = hash[salt_idx:chk_idx-1]315 chk = hash[chk_idx+1:]316 bare_salt = False317 else:318 # $-hash319 salt = hash[salt_idx:chk_idx]320 chk = hash[chk_idx+1:]321 bare_salt = True322 323 return cls(324 rounds=rounds,325 salt=salt,326 checksum=chk,327 bare_salt=bare_salt,328 )329 330 def to_string(self, _withchk=True):331 ss = u('') if self.bare_salt else u('$')332 rounds = self.rounds333 if rounds > 0:334 hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss)335 else:336 hash = u("$md5$%s%s") % (self.salt, ss)337 if _withchk:338 chk = self.checksum339 hash = u("%s$%s") % (hash, chk)340 return uascii_to_str(hash)341 342 #===================================================================343 # primary interface344 #===================================================================345 # TODO: if we're on solaris, check for native crypt() support.346 # this will require extra testing, to make sure native crypt347 # actually behaves correctly. of particular importance:348 # when using ""-config, make sure to append "$x" to string.349 350 def _calc_checksum(self, secret):351 # NOTE: no reference for how sun_md5_crypt handles unicode352 if isinstance(secret, unicode):353 secret = secret.encode("utf-8")354 config = str_to_bascii(self.to_string(_withchk=False))355 return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii")356 357 #===================================================================358 # eoc359 #===================================================================360 361#=============================================================================362# eof363#=============================================================================364 