diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-06-17 23:54:39 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-06-17 23:54:39 -0400 |
| commit | 0ad4f023487be628c7d163aa1e2c7775d3d94b5f (patch) | |
| tree | a232ac60dba19107ee7f8a55e313911e9f218efc | |
| parent | 6b2b7178a81af7c3f1a8574b04dfec42d0e05fba (diff) | |
| download | passlib-0ad4f023487be628c7d163aa1e2c7775d3d94b5f.tar.gz | |
converted remaining handlers to py3 compat (all unix crypt hashes)
| -rw-r--r-- | passlib/handlers/bcrypt.py | 62 | ||||
| -rw-r--r-- | passlib/handlers/des_crypt.py | 119 | ||||
| -rw-r--r-- | passlib/handlers/nthash.py | 32 | ||||
| -rw-r--r-- | passlib/handlers/sha1_crypt.py | 32 | ||||
| -rw-r--r-- | passlib/handlers/sha2_crypt.py | 125 | ||||
| -rw-r--r-- | passlib/handlers/sun_md5_crypt.py | 64 | ||||
| -rw-r--r-- | passlib/tests/test_drivers.py | 53 |
7 files changed, 285 insertions, 202 deletions
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index ae0ab1f..89fd05e 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -25,7 +25,7 @@ try: except ImportError: #pragma: no cover - though should run whole suite w/o bcryptor installed bcryptor_engine = None #libs -from passlib.utils import os_crypt, classproperty, handlers as uh, h64 +from passlib.utils import safe_os_crypt, classproperty, handlers as uh, h64, to_hash_str #pkg #local @@ -76,9 +76,9 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. checksum_size = 31 #--HasManyIdents-- - default_ident = "$2a$" - ident_values = ("$2$", "$2a$") - ident_aliases = {"2":"$2$", "2a": "$2a$"} + default_ident = u"$2a$" + ident_values = (u"$2$", u"$2a$") + ident_aliases = {u"2": u"$2$", u"2a": u"$2a$"} #--HasSalt-- min_salt_size = max_salt_size = 22 @@ -98,16 +98,16 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") for ident in cls.ident_values: if hash.startswith(ident): break else: raise ValueError("invalid bcrypt hash") - rounds, data = hash[len(ident):].split("$") + rounds, data = hash[len(ident):].split(u"$") rval = int(rounds) - if rounds != '%02d' % (rval,): + if rounds != u'%02d' % (rval,): raise ValueError("invalid bcrypt hash (no rounds padding)") salt, chk = data[:22], data[22:] return cls( @@ -118,8 +118,9 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. strict=bool(chk), ) - def to_string(self): - return "%s%02d$%s%s" % (self.ident, self.rounds, self.salt, self.checksum or '') + def to_string(self, native=True): + hash = u"%s%02d$%s%s" % (self.ident, self.rounds, self.salt, self.checksum or u'') + return to_hash_str(hash) if native else hash #========================================================= #primary interface @@ -137,13 +138,13 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. @classproperty def _has_backend_os_crypt(cls): return ( - os_crypt is not None + safe_os_crypt is not None and - os_crypt("test", "$2a$04$......................") == - '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty' + safe_os_crypt(u"test", u"$2a$04$......................")[1] == + u'$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty' and - os_crypt("test", "$2$04$......................") == - '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju' + safe_os_crypt(u"test", u"$2$04$......................")[1] == + u'$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju' ) @classmethod @@ -151,19 +152,38 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. return "no BCrypt backends available - please install pybcrypt or bcryptor for BCrypt support" def _calc_checksum_os_crypt(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - return os_crypt(secret, self.to_string())[-31:] + ok, hash = safe_os_crypt(secret, self.to_string(native=False)) + if ok: + return hash[-31:] + else: + #NOTE: not checking backends since this is lowest priority, + # so they probably aren't available either + raise ValueError("encoded password can't be handled by os_crypt" + " (recommend installing pybcrypt or bcryptor)") def _calc_checksum_pybcrypt(self, secret): + #pybcrypt behavior: + # py2: unicode secret -> ascii bytes (we override this) + # unicode hash -> ascii bytes (we provide ascii bytes) + # returns ascii bytes + # py3: can't get to install if isinstance(secret, unicode): secret = secret.encode("utf-8") - return pybcrypt_hashpw(secret, self.to_string())[-31:] + hash = pybcrypt_hashpw(secret, + self.to_string(native=False)) + return hash[-31:].decode("ascii") def _calc_checksum_bcryptor(self, secret): + #bcryptor behavior: + # py2: unicode secret -> ascii bytes (we have to override) + # unicode hash -> ascii bytes (we provide ascii bytes) + # returns ascii bytes + # py3: can't get to install if isinstance(secret, unicode): - secret = secret.encode("utf-8") - return bcryptor_engine(False).hash_key(secret, self.to_string())[-31:] + secret = secret.encode("utf-8") + hash = bcryptor_engine(False).hash_key(secret, + self.to_string(native=False)) + return hash[-31:].decode("ascii") #========================================================= #eoc diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py index 616e330..d916128 100644 --- a/passlib/handlers/des_crypt.py +++ b/passlib/handlers/des_crypt.py @@ -58,7 +58,8 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import h64, classproperty, os_crypt, handlers as uh +from passlib.utils import h64, classproperty, safe_os_crypt, b, bytes, \ + to_hash_str, handlers as uh, bord from passlib.utils.des import mdes_encrypt_int_block #pkg #local @@ -75,7 +76,7 @@ __all__ = [ def _crypt_secret_to_key(secret): "crypt helper which converts lower 7 bits of first 8 chars of secret -> 56-bit des key, padded to 64 bits" return sum( - (ord(c) & 0x7f) << (57-8*i) + (bord(c) & 0x7f) << (57-8*i) for i, c in enumerate(secret[:8]) ) @@ -111,7 +112,7 @@ def raw_ext_crypt(secret, rounds, salt): raise ValueError("invalid salt") #validate secret - if '\x00' in secret: #pragma: no cover - always caught by class + if b('\x00') in secret: #pragma: no cover - always caught by class #builtin linux crypt doesn't like this, so we don't either #XXX: would make more sense to raise ValueError, but want to be compatible w/ stdlib crypt raise ValueError("secret must be string without null bytes") @@ -171,7 +172,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): #========================================================= #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum - _pat = re.compile(r""" + _pat = re.compile(ur""" ^ (?P<salt>[./a-z0-9]{2}) (?P<chk>[./a-z0-9]{11})? @@ -179,19 +180,20 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): @classmethod def identify(cls, hash): - return bool(hash and cls._pat.match(hash)) + return uh.identify_regexp(hash, cls._pat) @classmethod def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") salt, chk = hash[:2], hash[2:] return cls(salt=salt, checksum=chk, strict=bool(chk)) - def to_string(self): - return "%s%s" % (self.salt, self.checksum or '') + def to_string(self, native=True): + hash = u"%s%s" % (self.salt, self.checksum or u'') + return to_hash_str(hash) if native else hash #========================================================= #backend @@ -202,25 +204,30 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): @classproperty def _has_backend_os_crypt(cls): - return os_crypt is not None and os_crypt("test", "ab") == 'abgOeLfPimXQo' + return safe_os_crypt and safe_os_crypt(u"test", u"ab")[1] == u'abgOeLfPimXQo' def _calc_checksum_builtin(self, secret): - #forbidding nul chars because linux crypt (and most C implementations) won't accept it either. - if '\x00' in secret: - raise ValueError("null char in secret") #gotta do something - no official policy since des-crypt predates unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") - return raw_crypt(secret, self.salt) + #forbidding nul chars because linux crypt (and most C implementations) won't accept it either. + if b('\x00') in secret: + raise ValueError("null char in secret") + return raw_crypt(secret, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): #os_crypt() would raise less useful error - if '\x00' in secret: + null = u'\x00' if isinstance(secret, unicode) else b('\x00') + if null in secret: raise ValueError("null char in secret") - #gotta do something - no official policy since des-crypt predates unicode - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - return os_crypt(secret, self.salt)[2:] + + #NOTE: safe_os_crypt encodes unicode secret -> utf8 + #no official policy since des-crypt predates unicode + ok, hash = safe_os_crypt(secret, self.salt) + if ok: + return hash[2:] + else: + return self._calc_checksum_builtin(secret) #========================================================= #eoc @@ -270,7 +277,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler salt_chars = uh.H64_CHARS #--HasRounds-- - default_rounds = 5000 + default_rounds = 5001 min_rounds = 0 max_rounds = 16777215 # (1<<24)-1 rounds_cost = "linear" @@ -281,7 +288,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler #========================================================= #internal helpers #========================================================= - _pat = re.compile(r""" + _pat = re.compile(ur""" ^ _ (?P<rounds>[./a-z0-9]{4}) @@ -291,27 +298,29 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler @classmethod def identify(cls, hash): - return bool(hash and cls._pat.match(hash)) + return uh.identify_regexp(hash, cls._pat) @classmethod def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") m = cls._pat.match(hash) if not m: raise ValueError("invalid ext-des-crypt hash") rounds, salt, chk = m.group("rounds", "salt", "chk") return cls( - rounds=h64.decode_int24(rounds), + rounds=h64.decode_int24(rounds.encode("ascii")), salt=salt, checksum=chk, strict=bool(chk), ) - def to_string(self): - return "_%s%s%s" % (h64.encode_int24(self.rounds), self.salt, self.checksum or '') + def to_string(self, native=True): + hash = u"_%s%s%s" % (h64.encode_int24(self.rounds).decode("ascii"), + self.salt, self.checksum or u'') + return to_hash_str(hash) if native else hash #========================================================= #backend @@ -322,18 +331,20 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler @classproperty def _has_backend_os_crypt(cls): - h = '_/...lLDAxARksGCHin.' - return os_crypt is not None and os_crypt("test", h) == h + h = u'_/...lLDAxARksGCHin.' + return safe_os_crypt and safe_os_crypt(u"test", h)[1] == h def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - return raw_ext_crypt(secret, self.rounds, self.salt) + return raw_ext_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - return os_crypt(secret, self.to_string())[9:] + ok, hash = safe_os_crypt(secret, self.salt) + if ok: + return hash[9:] + else: + return self._calc_checksum_builtin(secret) #========================================================= #eoc @@ -370,7 +381,7 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): #========================================================= #internal helpers #========================================================= - _pat = re.compile(r""" + _pat = re.compile(ur""" ^ (?P<salt>[./a-z0-9]{2}) (?P<chk>[./a-z0-9]{11,})? @@ -378,22 +389,30 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): @classmethod def identify(cls, hash): - return bool(hash and cls._pat.match(hash)) and (len(hash)-2) % 11 == 0 + if not hash: + return False + if isinstance(hash, bytes): + try: + hash = hash.decode("ascii") + except UnicodeDecodeError: + return False + return bool(cls._pat.match(hash)) and (len(hash)-2) % 11 == 0 @classmethod def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") m = cls._pat.match(hash) if not m: raise ValueError("invalid bigcrypt hash") salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk, strict=bool(chk)) - def to_string(self): - return "%s%s" % (self.salt, self.checksum or '') + def to_string(self, native=True): + hash = u"%s%s" % (self.salt, self.checksum or u'') + return to_hash_str(hash) if native else hash @classmethod def norm_checksum(cls, value, strict=False): @@ -410,14 +429,14 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): def calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - chk = raw_crypt(secret, self.salt) + chk = raw_crypt(secret, self.salt.encode("ascii")) idx = 8 end = len(secret) while idx < end: next = idx + 8 chk += raw_crypt(secret[idx:next], chk[-11:-9]) idx = next - return chk + return chk.decode("ascii") #========================================================= #eoc @@ -453,7 +472,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler): #========================================================= #internal helpers #========================================================= - _pat = re.compile(r""" + _pat = re.compile(ur""" ^ (?P<salt>[./a-z0-9]{2}) (?P<chk>[./a-z0-9]{22})? @@ -461,22 +480,23 @@ class crypt16(uh.HasSalt, uh.GenericHandler): @classmethod def identify(cls, hash): - return bool(hash and cls._pat.match(hash)) + return uh.identify_regexp(hash, cls._pat) @classmethod def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") m = cls._pat.match(hash) if not m: raise ValueError("invalid crypt16 hash") salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk, strict=bool(chk)) - def to_string(self): - return "%s%s" % (self.salt, self.checksum or '') + def to_string(self, native=True): + hash = u"%s%s" % (self.salt, self.checksum or u'') + return to_hash_str(hash) if native else hash #========================================================= #backend @@ -489,7 +509,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler): #parse salt value try: - salt_value = h64.decode_int12(self.salt) + salt_value = h64.decode_int12(self.salt.encode("ascii")) except ValueError: #pragma: no cover - caught by class raise ValueError("invalid chars in salt") @@ -506,7 +526,8 @@ class crypt16(uh.HasSalt, uh.GenericHandler): result2 = mdes_encrypt_int_block(key2, 0, salt_value, 5) #done - return h64.encode_dc_int64(result1) + h64.encode_dc_int64(result2) + chk = h64.encode_dc_int64(result1) + h64.encode_dc_int64(result2) + return chk.decode("ascii") #========================================================= #eoc diff --git a/passlib/handlers/nthash.py b/passlib/handlers/nthash.py index de06025..33d6f25 100644 --- a/passlib/handlers/nthash.py +++ b/passlib/handlers/nthash.py @@ -8,7 +8,7 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import handlers as uh +from passlib.utils import handlers as uh, to_unicode, to_hash_str, to_bytes from passlib.utils.md4 import md4 #pkg #local @@ -41,12 +41,12 @@ class nthash(uh.HasManyIdents, uh.GenericHandler): setting_kwds = ("ident",) checksum_chars = uh.LC_HEX_CHARS - _stub_checksum = "0" * 32 + _stub_checksum = u"0" * 32 #--HasManyIdents-- - default_ident = "$3$$" - ident_values = ("$3$$", "$NT$") - ident_aliases = {"3": "$3$$", "NT": "$NT$"} + default_ident = u"$3$$" + ident_values = (u"$3$$", u"$NT$") + ident_aliases = {u"3": u"$3$$", u"NT": u"$NT$"} #========================================================= #formatting @@ -56,8 +56,8 @@ class nthash(uh.HasManyIdents, uh.GenericHandler): def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") for ident in cls.ident_values: if hash.startswith(ident): break @@ -67,22 +67,30 @@ class nthash(uh.HasManyIdents, uh.GenericHandler): return cls(ident=ident, checksum=chk, strict=True) def to_string(self): - return self.ident + (self.checksum or self._stub_checksum) + hash = self.ident + (self.checksum or self._stub_checksum) + return to_hash_str(hash) #========================================================= #primary interface #========================================================= def calc_checksum(self, secret): - if secret is None: - raise TypeError("secret must be a string") return self.raw_nthash(secret, hex=True) @staticmethod def raw_nthash(secret, hex=False): - "encode password using md4-based NTHASH algorithm; returns string of raw bytes" + """encode password using md4-based NTHASH algorithm + + :returns: + returns string of raw bytes if ``hex=False``, + returns digest as hexidecimal unicode if ``hex=True``. + """ + secret = to_unicode(secret, "utf-8") hash = md4(secret.encode("utf-16le")) - return hash.hexdigest() if hex else hash.digest() + if hex: + return to_unicode(hash.hexdigest(), 'ascii') + else: + return hash.digest() #========================================================= #eoc diff --git a/passlib/handlers/sha1_crypt.py b/passlib/handlers/sha1_crypt.py index 6ad4e02..8273000 100644 --- a/passlib/handlers/sha1_crypt.py +++ b/passlib/handlers/sha1_crypt.py @@ -13,7 +13,8 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import h64, handlers as uh, os_crypt, classproperty +from passlib.utils import h64, handlers as uh, safe_os_crypt, classproperty, \ + to_hash_str, to_unicode, bytes, b from passlib.utils.pbkdf2 import hmac_sha1 #pkg #local @@ -56,7 +57,7 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler #--GenericHandler-- name = "sha1_crypt" setting_kwds = ("salt", "salt_size", "rounds") - ident = "$sha1$" + ident = u"$sha1$" checksum_size = 28 #--HasSalt-- @@ -87,11 +88,11 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler strict=bool(chk), ) - def to_string(self): - out = "$sha1$%d$%s" % (self.rounds, self.salt) + def to_string(self, native=True): + out = u"$sha1$%d$%s" % (self.rounds, self.salt) if self.checksum: - out += "$" + self.checksum - return out + out += u"$" + self.checksum + return to_hash_str(out) if native else out #========================================================= #backend @@ -102,19 +103,21 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler @classproperty def _has_backend_os_crypt(cls): - h = '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHimExLaiSFlGkAe' - return os_crypt is not None and os_crypt("test", h) == h + h = u'$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHimExLaiSFlGkAe' + return safe_os_crypt and safe_os_crypt(u"test", h)[1] == h def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") rounds = self.rounds - result = "%s$sha1$%s" % (self.salt, rounds) + #NOTE: this uses a different format than the hash... + result = u"%s$sha1$%s" % (self.salt, rounds) + result = result.encode("ascii") r = 0 while r < rounds: result = hmac_sha1(secret, result) r += 1 - return h64.encode_transposed_bytes(result, self._chk_offsets) + return h64.encode_transposed_bytes(result, self._chk_offsets).decode("ascii") _chk_offsets = [ 2,1,0, @@ -127,10 +130,11 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler ] def _calc_checksum_os_crypt(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - h = os_crypt(secret, self.to_string()) - return h[h.rindex("$")+1:] + ok, hash = safe_os_crypt(secret, self.to_string(native=False)) + if ok: + return hash[hash.rindex("$")+1:] + else: + return self._calc_checksum_builtin(secret) #========================================================= #eoc diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py index 56e72e9..58effd8 100644 --- a/passlib/handlers/sha2_crypt.py +++ b/passlib/handlers/sha2_crypt.py @@ -9,7 +9,8 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import h64, os_crypt, classproperty, handlers as uh +from passlib.utils import h64, safe_os_crypt, classproperty, handlers as uh, \ + to_hash_str, to_unicode, bytes, b, bord #pkg #local __all__ = [ @@ -20,6 +21,8 @@ __all__ = [ #========================================================= #pure-python backend (shared between sha256-crypt & sha512-crypt) #========================================================= +INVALID_SALT_VALUES = b("\x00$") + def raw_sha_crypt(secret, salt, rounds, hash): """perform raw sha crypt @@ -33,8 +36,8 @@ def raw_sha_crypt(secret, salt, rounds, hash): """ #validate secret - if isinstance(secret, unicode): - secret = secret.encode("utf-8") + if not isinstance(secret, bytes): + raise TypeError("secret must be encoded as bytes") #validate rounds if rounds < 1000: @@ -43,7 +46,9 @@ def raw_sha_crypt(secret, salt, rounds, hash): rounds = 999999999 #validate salt - if any(c in salt for c in '\x00$'): + if not isinstance(salt, bytes): + raise TypeError("salt must be encoded as bytes") + if any(c in salt for c in INVALID_SALT_VALUES): raise ValueError("invalid chars in salt") if len(salt) > 16: salt = salt[:16] @@ -86,7 +91,7 @@ def raw_sha_crypt(secret, salt, rounds, hash): dp_result = extend(dp.digest(), secret) #calc DS - hash of salt, extended to size of salt - ds = hash(salt * (16+ord(a_result[0]))) + ds = hash(salt * (16+bord(a_result[0]))) ds_result = extend(ds.digest(), salt) #aka 'S' # @@ -242,7 +247,7 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl #--GenericHandler-- name = "sha256_crypt" setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size") - ident = "$5$" + ident = u"$5$" #--HasSalt-- min_salt_size = 0 @@ -270,7 +275,7 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl #========================================================= #: regexp used to parse hashes - _pat = re.compile(r""" + _pat = re.compile(ur""" ^ \$5 (\$rounds=(?P<rounds>\d+))? @@ -289,13 +294,13 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") m = cls._pat.match(hash) if not m: raise ValueError("invalid sha256-crypt hash") rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk") - if rounds and rounds.startswith("0"): + if rounds and rounds.startswith(u"0"): raise ValueError("invalid sha256-crypt hash (zero-padded rounds)") return cls( implicit_rounds = not rounds, @@ -305,11 +310,12 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl strict=bool(chk), ) - def to_string(self): + def to_string(self, native=True): if self.rounds == 5000 and self.implicit_rounds: - return "$5$%s$%s" % (self.salt, self.checksum or '') + hash = u"$5$%s$%s" % (self.salt, self.checksum or u'') else: - return "$5$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '') + hash = u"$5$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or u'') + return to_hash_str(hash) if native else hash #========================================================= #backend @@ -321,27 +327,34 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl @classproperty def _has_backend_os_crypt(cls): return bool( - os_crypt is not None and - os_crypt("test", "$5$rounds=1000$test") == - "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/" + safe_os_crypt and + safe_os_crypt(u"test", u"$5$rounds=1000$test")[1] == + u"$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/" ) def _calc_checksum_builtin(self, secret): - checksum, salt, rounds = raw_sha256_crypt(secret, self.salt, self.rounds) - assert salt == self.salt, "class doesn't agree w/ builtin backend" - assert rounds == self.rounds, "class doesn't agree w/ builtin backend" - return checksum + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + checksum, salt, rounds = raw_sha256_crypt(secret, + self.salt.encode("ascii"), + self.rounds) + assert salt == self.salt.encode("ascii"), \ + "class doesn't agree w/ builtin backend: salt %r != %r" % (salt, self.salt.encode("ascii")) + assert rounds == self.rounds, \ + "class doesn't agree w/ builtin backend: rounds %r != %r" % (rounds, self.rounds) + return checksum.decode("ascii") def _calc_checksum_os_crypt(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - #NOTE: avoiding full parsing routine via from_string().checksum, - # and just extracting the bit we need. - result = os_crypt(secret, self.to_string()) - assert result.startswith("$5$") - chk = result[-43:] - assert '$' not in chk - return chk + ok, result = safe_os_crypt(secret, self.to_string(native=False)) + if ok: + #NOTE: avoiding full parsing routine via from_string().checksum, + # and just extracting the bit we need. + assert result.startswith(u"$5$") + chk = result[-43:] + assert u'$' not in chk + return chk + else: + return self._calc_checksum_builtin(secret) #========================================================= #eoc @@ -386,7 +399,7 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl #algorithm information #========================================================= name = "sha512_crypt" - ident = "$6$" + ident = u"$6$" setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size") @@ -414,7 +427,7 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl #========================================================= #: regexp used to parse hashes - _pat = re.compile(r""" + _pat = re.compile(ur""" ^ \$6 (\$rounds=(?P<rounds>\d+))? @@ -435,8 +448,8 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") m = cls._pat.match(hash) if not m: raise ValueError("invalid sha512-crypt hash") @@ -451,11 +464,12 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl strict=bool(chk), ) - def to_string(self): + def to_string(self, native=True): if self.rounds == 5000 and self.implicit_rounds: - return "$6$%s$%s" % (self.salt, self.checksum or '') + hash = u"$6$%s$%s" % (self.salt, self.checksum or u'') else: - return "$6$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '') + hash = u"$6$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or u'') + return to_hash_str(hash) if native else hash #========================================================= #backend @@ -467,29 +481,36 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl @classproperty def _has_backend_os_crypt(cls): return bool( - os_crypt is not None and - os_crypt("test", "$6$rounds=1000$test") == - "$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0" + safe_os_crypt and + safe_os_crypt(u"test", u"$6$rounds=1000$test")[1] == + u"$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0" ) #NOTE: testing w/ HashTimer shows 64-bit linux's crypt to be ~2.6x faster than builtin (627253 vs 238152 rounds/sec) def _calc_checksum_builtin(self, secret): - checksum, salt, rounds = raw_sha512_crypt(secret, self.salt, self.rounds) - assert salt == self.salt, "class doesn't agree w/ builtin backend" - assert rounds == self.rounds, "class doesn't agree w/ builtin backend" - return checksum - - def _calc_checksum_os_crypt(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - #NOTE: avoiding full parsing routine via from_string().checksum, - # and just extracting the bit we need. - result = os_crypt(secret, self.to_string()) - assert result.startswith("$6$") - chk = result[-86:] - assert '$' not in chk - return chk + checksum, salt, rounds = raw_sha512_crypt(secret, + self.salt.encode("ascii"), + self.rounds) + assert salt == self.salt.encode("ascii"), \ + "class doesn't agree w/ builtin backend: salt %r != %r" % (salt, self.salt.encode("ascii")) + assert rounds == self.rounds, \ + "class doesn't agree w/ builtin backend: rounds %r != %r" % (rounds, self.rounds) + return checksum.decode("ascii") + + def _calc_checksum_os_crypt(self, secret): + ok, result = safe_os_crypt(secret, self.to_string(native=False)) + if ok: + #NOTE: avoiding full parsing routine via from_string().checksum, + # and just extracting the bit we need. + assert result.startswith(u"$6$") + chk = result[-86:] + assert u'$' not in chk + return chk + else: + return self._calc_checksum_builtin(secret) #========================================================= #eoc diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py index 4306b5c..9bcb9f7 100644 --- a/passlib/handlers/sun_md5_crypt.py +++ b/passlib/handlers/sun_md5_crypt.py @@ -17,7 +17,7 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import h64, handlers as uh +from passlib.utils import h64, handlers as uh, to_hash_str, to_unicode, bytes, b, bord #pkg #local __all__ = [ @@ -31,7 +31,7 @@ __all__ = [ # exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt # from Project Gutenberg. -MAGIC_HAMLET = ( +MAGIC_HAMLET = b( "To be, or not to be,--that is the question:--\n" "Whether 'tis nobler in the mind to suffer\n" "The slings and arrows of outrageous fortune\n" @@ -82,12 +82,8 @@ del xr def raw_sun_md5_crypt(secret, rounds, salt): "given secret & salt, return encoded sun-md5-crypt checksum" global MAGIC_HAMLET - - #validate secret - #FIXME: no definitive information about how it handles unicode, - # so using this as a fallback... - if isinstance(secret, unicode): - secret = secret.encode("utf-8") + assert isinstance(secret, bytes) + assert isinstance(salt, bytes) #validate rounds if rounds <= 0: @@ -117,7 +113,7 @@ def raw_sun_md5_crypt(secret, rounds, salt): round = 0 while round < real_rounds: #convert last result byte string to list of byte-ints for easy access - rval = [ ord(c) for c in result ].__getitem__ + rval = [ bord(c) for c in result ].__getitem__ #build up X bit by bit x = 0 @@ -144,7 +140,7 @@ def raw_sun_md5_crypt(secret, rounds, salt): h = md5(result) if coin: h.update(MAGIC_HAMLET) - h.update(str(round)) + h.update(unicode(round).encode("ascii")) result = h.digest() round += 1 @@ -233,25 +229,32 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): #========================================================= @classmethod def identify(cls, hash): - return bool(hash) and (hash.startswith("$md5$") or hash.startswith("$md5,")) + if not hash: + return False + if isinstance(hash, bytes): + try: + hash = hash.decode("ascii") + except UnicodeDecodeError: + return False + return hash.startswith(u"$md5$") or hash.startswith(u"$md5,") @classmethod def from_string(cls, hash): if not hash: raise ValueError("no hash specified") - if isinstance(hash, unicode): - hash = hash.encode("ascii") + if isinstance(hash, bytes): + hash = hash.decode("ascii") # #detect if hash specifies rounds value. #if so, parse and validate it. #by end, set 'rounds' to int value, and 'tail' containing salt+chk # - if hash.startswith("$md5$"): + if hash.startswith(u"$md5$"): rounds = 0 salt_idx = 5 - elif hash.startswith("$md5,rounds="): - idx = hash.find("$", 12) + elif hash.startswith(u"$md5,rounds="): + idx = hash.find(u"$", 12) if idx == -1: raise ValueError("invalid sun-md5-crypt hash (unexpected end of rounds)") rstr = hash[12:idx] @@ -259,7 +262,7 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): rounds = int(rstr) except ValueError: raise ValueError("invalid sun-md5-crypt hash (bad rounds)") - if rstr != str(rounds): + if rstr != unicode(rounds): raise ValueError("invalid sun-md5-crypt hash (zero-padded rounds)") if rounds == 0: #NOTE: not sure if this is *forbidden* precisely, @@ -275,20 +278,20 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): #to deal cleanly with some backward-compatible workarounds #implemented by original implementation. # - chk_idx = hash.rfind("$", salt_idx) + chk_idx = hash.rfind(u"$", salt_idx) if chk_idx == -1: # ''-config for $-hash salt = hash[salt_idx:] chk = None bare_salt = True elif chk_idx == len(hash)-1: - if chk_idx > salt_idx and hash[-2] == "$": + if chk_idx > salt_idx and hash[-2] == u"$": raise ValueError("invalid sun-md5-crypt hash (too many $)") # $-config for $$-hash salt = hash[salt_idx:-1] chk = None bare_salt = False - elif chk_idx > 0 and hash[chk_idx-1] == "$": + elif chk_idx > 0 and hash[chk_idx-1] == u"$": # $$-hash salt = hash[salt_idx:chk_idx-1] chk = hash[chk_idx+1:] @@ -307,18 +310,18 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): strict=bool(chk), ) - def to_string(self, withchk=True): - ss = '' if self.bare_salt else '$' + def to_string(self, withchk=True, native=True): + ss = u'' if self.bare_salt else u'$' rounds = self.rounds if rounds > 0: - out = "$md5,rounds=%d$%s%s" % (rounds, self.salt, ss) + out = u"$md5,rounds=%d$%s%s" % (rounds, self.salt, ss) else: - out = "$md5$%s%s" % (self.salt, ss) + out = u"$md5$%s%s" % (self.salt, ss) if withchk: chk = self.checksum if chk: - out = "%s$%s" % (out, chk) - return out + out = u"%s$%s" % (out, chk) + return to_hash_str(out) if native else out #========================================================= #primary interface @@ -329,8 +332,13 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): # especially, when using ''-config, make sure to append '$x' to string. def calc_checksum(self, secret): - config = self.to_string(withchk=False) - return raw_sun_md5_crypt(secret, self.rounds, config) + #NOTE: no reference for how sun_md5_crypt handles unicode + if secret is None: + raise TypeError("no secret specified") + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + config = self.to_string(withchk=False,native=False).encode("ascii") + return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii") #========================================================= #eoc diff --git a/passlib/tests/test_drivers.py b/passlib/tests/test_drivers.py index 23c2e96..e40a783 100644 --- a/passlib/tests/test_drivers.py +++ b/passlib/tests/test_drivers.py @@ -36,17 +36,18 @@ class AprMd5CryptTest(HandlerCase): from passlib.handlers.bcrypt import bcrypt class BCryptTest(HandlerCase): + handler = bcrypt secret_chars = 72 - known_correct_hashes = ( + known_correct_hashes = [ #selected bcrypt test vectors ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'), ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'), ('abcdefghijklmnopqrstuvwxyz', '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), ('~!@#$%^&*() ~!@#$%^&*()PNBFRD', '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'), - ) + ] known_unidentified_hashes = [ #unsupported minor version @@ -73,20 +74,20 @@ class BCryptTest(HandlerCase): kwds['ident'] = 'Q' self.assertRaises(ValueError, handler, **kwds) - #this method is added in order to maximize test coverage on systems - #where os_crypt is missing or doesn't support bcrypt - if enable_option("cover") and not bcrypt.has_backend("os_crypt") and bcrypt.has_backend("pybcrypt"): - def test_backend(self): - from passlib.handlers import bcrypt as bcrypt_mod - orig = bcrypt_mod.os_crypt - bcrypt_mod.os_crypt = bcrypt_mod.pybcrypt_hashpw - orig = bcrypt.get_backend() - try: - bcrypt.set_backend("os_crypt") - bcrypt.encrypt(u"test", rounds=4) - finally: - bcrypt.set_backend(orig) - bcrypt_mod.os_crypt = orig + ###this method is added in order to maximize test coverage on systems + ###where os_crypt is missing or doesn't support bcrypt + ##if enable_option("cover") and not bcrypt.has_backend("os_crypt") and bcrypt.has_backend("pybcrypt"): + ## def test_backend(self): + ## from passlib.handlers import bcrypt as bcrypt_mod + ## orig = bcrypt_mod.safe_os_crypt + ## bcrypt_mod.safe_os_crypt = bcrypt_mod.pybcrypt_hashpw + ## orig = bcrypt.get_backend() + ## try: + ## bcrypt.set_backend("os_crypt") + ## bcrypt.encrypt(u"test", rounds=4) + ## finally: + ## bcrypt.set_backend(orig) + ## bcrypt_mod.os_crypt = orig bcrypt._no_backends_msg() #call this for coverage purposes @@ -174,7 +175,7 @@ class DesCryptTest(HandlerCase): handler = des_crypt secret_chars = 8 - known_correct_hashes = ( + known_correct_hashes = [ #secret, example hash which matches secret ('', 'OgAwTx2l6NADI'), (' ', '/Hk.VPuwQTXbc'), @@ -183,7 +184,7 @@ class DesCryptTest(HandlerCase): ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'), ('AlOtBsOl', 'cEpWz5IUCShqM'), (u'hell\u00D6', 'saykDgk3BPZ9E'), - ) + ] known_unidentified_hashes = [ #bad char in otherwise correctly formatted hash '!gAwTx2l6NADI', @@ -423,10 +424,10 @@ from passlib.handlers.nthash import nthash class NTHashTest(HandlerCase): handler = nthash - known_correct_hashes = ( + known_correct_hashes = [ ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'), ('passphrase', '$NT$7f8fe03093cc84b267b109625f6bbf4b'), - ) + ] known_malformed_hashes = [ #bad char in otherwise correct hash @@ -740,10 +741,10 @@ from passlib.handlers.sha1_crypt import sha1_crypt class SHA1CryptTest(HandlerCase): handler = sha1_crypt - known_correct_hashes = ( + known_correct_hashes = [ ("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"), ("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"), - ) + ] known_malformed_hashes = [ #bad char in otherwise correct hash @@ -852,12 +853,12 @@ class SHA256CryptTest(HandlerCase): def test_raw(self): #run some tests on raw backend func to ensure it works right self.assertEqual( - raw_sha_crypt('secret', 'salt'*10, 1, hashlib.md5), - ('\x1f\x96\x1cO\x11\xa9h\x12\xc4\xf3\x9c\xee\xf5\x93\xf3\xdd', - 'saltsaltsaltsalt', + raw_sha_crypt(b('secret'), b('salt')*10, 1, hashlib.md5), + (b('\x1f\x96\x1cO\x11\xa9h\x12\xc4\xf3\x9c\xee\xf5\x93\xf3\xdd'), + b('saltsaltsaltsalt'), 1000) ) - self.assertRaises(ValueError, raw_sha_crypt, 'secret', '$', 1, hashlib.md5) + self.assertRaises(ValueError, raw_sha_crypt, b('secret'), b('$'), 1, hashlib.md5) BuiltinSHA256CryptTest = create_backend_case(SHA256CryptTest, "builtin") |
