def test_10_identify(self): """test GenericHandler.identify()""" class d1(uh.GenericHandler): @classmethod def from_string(cls, hash): if isinstance(hash, bytes): hash = hash.decode("ascii") if hash == u('a'): return cls(checksum=hash) else: raise ValueError # check fallback self.assertRaises(TypeError, d1.identify, None) self.assertRaises(TypeError, d1.identify, 1) self.assertFalse(d1.identify('')) self.assertTrue(d1.identify('a')) self.assertFalse(d1.identify('b')) # check regexp d1._hash_regex = re.compile(u('@.')) self.assertRaises(TypeError, d1.identify, None) self.assertRaises(TypeError, d1.identify, 1) self.assertTrue(d1.identify('@a')) self.assertFalse(d1.identify('a')) del d1._hash_regex # check ident-based d1.ident = u('!') self.assertRaises(TypeError, d1.identify, None) self.assertRaises(TypeError, d1.identify, 1) self.assertTrue(d1.identify('!a')) self.assertFalse(d1.identify('a')) del d1.ident
def test_to_native_str(self): "test to_native_str()" from passlib.utils import to_native_str # test plain ascii self.assertEqual(to_native_str(u('abc'), 'ascii'), 'abc') self.assertEqual(to_native_str(b('abc'), 'ascii'), 'abc') # test invalid ascii if PY3: self.assertEqual(to_native_str(u('\xE0'), 'ascii'), '\xE0') self.assertRaises(UnicodeDecodeError, to_native_str, b('\xC3\xA0'), 'ascii') else: self.assertRaises(UnicodeEncodeError, to_native_str, u('\xE0'), 'ascii') self.assertEqual(to_native_str(b('\xC3\xA0'), 'ascii'), '\xC3\xA0') # test latin-1 self.assertEqual(to_native_str(u('\xE0'), 'latin-1'), '\xE0') self.assertEqual(to_native_str(b('\xE0'), 'latin-1'), '\xE0') # test utf-8 self.assertEqual(to_native_str(u('\xE0'), 'utf-8'), '\xE0' if PY3 else '\xC3\xA0') self.assertEqual(to_native_str(b('\xC3\xA0'), 'utf-8'), '\xE0' if PY3 else '\xC3\xA0') # other types rejected self.assertRaises(TypeError, to_native_str, None, 'ascii')
def test_ab64_decode(self): """ab64_decode()""" from passlib.utils.binary import ab64_decode # accept bytes or unicode self.assertEqual(ab64_decode(b"abc"), hb("69b7")) self.assertEqual(ab64_decode(u("abc")), hb("69b7")) # reject non-ascii unicode self.assertRaises(ValueError, ab64_decode, u("ab\xff")) # underlying a2b_ascii treats non-base64 chars as "Incorrect padding" self.assertRaises(TypeError, ab64_decode, b"ab\xff") self.assertRaises(TypeError, ab64_decode, b"ab!") self.assertRaises(TypeError, ab64_decode, u("ab!")) # insert correct padding, handle dirty padding bits self.assertEqual(ab64_decode(b"abcd"), hb("69b71d")) # 0 mod 4 self.assertRaises(ValueError, ab64_decode, b"abcde") # 1 mod 4 self.assertEqual(ab64_decode(b"abcdef"), hb("69b71d79")) # 2 mod 4, dirty padding bits self.assertEqual(ab64_decode(b"abcdeQ"), hb("69b71d79")) # 2 mod 4, clean padding bits self.assertEqual(ab64_decode(b"abcdefg"), hb("69b71d79f8")) # 3 mod 4, clean padding bits # support "./" or "+/" altchars # (lets us transition to "+/" representation, merge w/ b64s_decode) self.assertEqual(ab64_decode(b"ab+/"), hb("69bfbf")) self.assertEqual(ab64_decode(b"ab./"), hb("69bfbf"))
def test_to_bytes(self): "test to_bytes()" from passlib.utils import to_bytes # check unicode inputs self.assertEqual(to_bytes(u('abc')), b('abc')) self.assertEqual(to_bytes(u('\x00\xff')), b('\x00\xc3\xbf')) # check unicode w/ encodings self.assertEqual(to_bytes(u('\x00\xff'), 'latin-1'), b('\x00\xff')) self.assertRaises(ValueError, to_bytes, u('\x00\xff'), 'ascii') # check bytes inputs self.assertEqual(to_bytes(b('abc')), b('abc')) self.assertEqual(to_bytes(b('\x00\xff')), b('\x00\xff')) self.assertEqual(to_bytes(b('\x00\xc3\xbf')), b('\x00\xc3\xbf')) # check byte inputs ignores enocding self.assertEqual(to_bytes(b('\x00\xc3\xbf'), "latin-1"), b('\x00\xc3\xbf')) # check bytes transcoding self.assertEqual(to_bytes(b('\x00\xc3\xbf'), "latin-1", "", "utf-8"), b('\x00\xff')) # check other self.assertRaises(AssertionError, to_bytes, 'abc', None) self.assertRaises(TypeError, to_bytes, None)
def test_is_ascii_safe(self): "test is_ascii_safe()" from passlib.utils import is_ascii_safe self.assertTrue(is_ascii_safe(b("\x00abc\x7f"))) self.assertTrue(is_ascii_safe(u("\x00abc\x7f"))) self.assertFalse(is_ascii_safe(b("\x00abc\x80"))) self.assertFalse(is_ascii_safe(u("\x00abc\x80")))
def test_02_handler_wrapper(self): """test Hasher-compatible handler wrappers""" from passlib.ext.django.utils import get_passlib_hasher from django.contrib.auth import hashers # should return native django hasher if available hasher = get_passlib_hasher("hex_md5") self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher) hasher = get_passlib_hasher("django_bcrypt") self.assertIsInstance(hasher, hashers.BCryptPasswordHasher) # otherwise should return wrapper from passlib.hash import sha256_crypt hasher = get_passlib_hasher("sha256_crypt") self.assertEqual(hasher.algorithm, "passlib_sha256_crypt") # and wrapper should return correct hash encoded = hasher.encode("stub") self.assertTrue(sha256_crypt.verify("stub", encoded)) self.assertTrue(hasher.verify("stub", encoded)) self.assertFalse(hasher.verify("xxxx", encoded)) # test wrapper accepts options encoded = hasher.encode("stub", "abcd"*4, iterations=1234) self.assertEqual(encoded, "$5$rounds=1234$abcdabcdabcdabcd$" "v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6") self.assertEqual(hasher.safe_summary(encoded), {'algorithm': 'sha256_crypt', 'salt': u('abcdab**********'), 'iterations': 1234, 'hash': u('v2RWkZ*************************************'), })
def to_string(self): hash = u("%s%s%s%s") % ( self.ident, h64.encode_int6(self.rounds).decode("ascii"), self.salt, self.checksum or u(""), ) return uascii_to_str(hash)
def to_string(self): if self.rounds == 5000 and self.implicit_rounds: hash = u("%s%s$%s") % (self.ident, self.salt, self.checksum or u('')) else: hash = u("%srounds=%d$%s$%s") % (self.ident, self.rounds, self.salt, self.checksum or u('')) return uascii_to_str(hash)
def test_lmhash(self): from passlib.win32 import raw_lmhash for secret, hash in [ ("OLDPASSWORD", u("c9b81d939d6fd80cd408e6b105741864")), ("NEWPASSWORD", u('09eeab5aa415d6e4d408e6b105741864')), ("welcome", u("c23413a8a1e7665faad3b435b51404ee")), ]: result = raw_lmhash(secret, hex=True) self.assertEqual(result, hash)
def test_nthash(self): warnings.filterwarnings("ignore", r"nthash\.raw_nthash\(\) is deprecated") from passlib.win32 import raw_nthash for secret, hash in [ ("OLDPASSWORD", u("6677b2c394311355b54f25eec5bfacf5")), ("NEWPASSWORD", u("256781a62031289d3c2c98c14f1efc8c")), ]: result = raw_nthash(secret, hex=True) self.assertEqual(result, hash)
def from_string(cls, hash): ident, tail = cls._parse_ident(hash) if ident == IDENT_2X: raise ValueError("crypt_blowfish's buggy '2x' hashes are not " "currently supported") rounds_str, data = tail.split(u("$")) rounds = int(rounds_str) if rounds_str != u("%02d") % (rounds,): raise uh.exc.MalformedHashError(cls, "malformed cost field") salt, chk = data[:22], data[22:] return cls(rounds=rounds, salt=salt, checksum=chk or None, ident=ident)
def test_11_norm_checksum(self): """test GenericHandler checksum handling""" # setup helpers class d1(uh.GenericHandler): name = 'd1' checksum_size = 4 checksum_chars = u('xz') _stub_checksum = u('z')*4 def norm_checksum(*a, **k): return d1(*a, **k).checksum # too small self.assertRaises(ValueError, norm_checksum, u('xxx')) # right size self.assertEqual(norm_checksum(u('xxxx')), u('xxxx')) self.assertEqual(norm_checksum(u('xzxz')), u('xzxz')) # too large self.assertRaises(ValueError, norm_checksum, u('xxxxx')) # wrong chars self.assertRaises(ValueError, norm_checksum, u('xxyx')) # wrong type self.assertRaises(TypeError, norm_checksum, b'xxyx') # relaxed with self.assertWarningList("checksum should be unicode"): self.assertEqual(norm_checksum(b'xxzx', relaxed=True), u('xxzx')) self.assertRaises(TypeError, norm_checksum, 1, relaxed=True) # test _stub_checksum behavior self.assertIs(norm_checksum(u('zzzz')), None)
def test_91_parsehash(self): """test parsehash()""" # NOTE: this just tests some existing GenericHandler classes from passlib import hash # # parsehash() # # simple hash w/ salt result = hash.des_crypt.parsehash("OgAwTx2l6NADI") self.assertEqual(result, {'checksum': u('AwTx2l6NADI'), 'salt': u('Og')}) # parse rounds and extra implicit_rounds flag h = '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9' s = u('LKO/Ute40T3FNF95') c = u('U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9') result = hash.sha256_crypt.parsehash(h) self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True, checksum=c)) # omit checksum result = hash.sha256_crypt.parsehash(h, checksum=False) self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True)) # sanitize result = hash.sha256_crypt.parsehash(h, sanitize=True) self.assertEqual(result, dict(rounds=5000, implicit_rounds=True, salt=u('LK**************'), checksum=u('U0pr***************************************'))) # parse w/o implicit rounds flag result = hash.sha256_crypt.parsehash('$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3') self.assertEqual(result, dict( checksum=u('YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'), salt=u('uy/jIAhCetNCTtb0'), rounds=10428, )) # parsing of raw checksums & salts h1 = '$pbkdf2$60000$DoEwpvQeA8B4T.k951yLUQ$O26Y3/NJEiLCVaOVPxGXshyjW8k' result = hash.pbkdf2_sha1.parsehash(h1) self.assertEqual(result, dict( checksum=b';n\x98\xdf\xf3I\x12"\xc2U\xa3\x95?\x11\x97\xb2\x1c\xa3[\xc9', rounds=60000, salt=b'\x0e\x810\xa6\xf4\x1e\x03\xc0xO\xe9=\xe7\\\x8bQ', )) # sanitizing of raw checksums & salts result = hash.pbkdf2_sha1.parsehash(h1, sanitize=True) self.assertEqual(result, dict( checksum=u('O26************************'), rounds=60000, salt=u('Do********************'), ))
def to_string(self, _withchk=True): ss = u('') if self.bare_salt else u('$') rounds = self.rounds if rounds > 0: hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss) else: hash = u("$md5$%s%s") % (self.salt, ss) if _withchk: chk = self.checksum hash = u("%s$%s") % (hash, chk) return uascii_to_str(hash)
def test_11_norm_checksum(self): """test GenericHandler checksum handling""" # setup helpers class d1(uh.GenericHandler): name = 'd1' checksum_size = 4 checksum_chars = u('xz') def norm_checksum(checksum=None, **k): return d1(checksum=checksum, **k).checksum # too small self.assertRaises(ValueError, norm_checksum, u('xxx')) # right size self.assertEqual(norm_checksum(u('xxxx')), u('xxxx')) self.assertEqual(norm_checksum(u('xzxz')), u('xzxz')) # too large self.assertRaises(ValueError, norm_checksum, u('xxxxx')) # wrong chars self.assertRaises(ValueError, norm_checksum, u('xxyx')) # wrong type self.assertRaises(TypeError, norm_checksum, b'xxyx') # relaxed # NOTE: this could be turned back on if we test _norm_checksum() directly... #with self.assertWarningList("checksum should be unicode"): # self.assertEqual(norm_checksum(b'xxzx', relaxed=True), u('xxzx')) #self.assertRaises(TypeError, norm_checksum, 1, relaxed=True) # test _stub_checksum behavior self.assertEqual(d1()._stub_checksum, u('xxxx'))
def test_norm_hash_name(self): "test norm_hash_name()" from itertools import chain from passlib.utils.pbkdf2 import norm_hash_name, _nhn_hash_names # test formats for format in self.ndn_formats: norm_hash_name("md4", format) self.assertRaises(ValueError, norm_hash_name, "md4", None) self.assertRaises(ValueError, norm_hash_name, "md4", "fake") # test types self.assertEqual(norm_hash_name(u("MD4")), "md4") self.assertEqual(norm_hash_name(b("MD4")), "md4") self.assertRaises(TypeError, norm_hash_name, None) # test selected results with catch_warnings(): warnings.filterwarnings("ignore", ".*unknown hash") for row in chain(_nhn_hash_names, self.ndn_values): for idx, format in enumerate(self.ndn_formats): correct = row[idx] for value in row: result = norm_hash_name(value, format) self.assertEqual(result, correct, "name=%r, format=%r:" % (value, format))
def _calc_checksum(self, secret): # NOTE: this bypasses bcrypt's _calc_checksum, # so has to take care of all it's issues, such as secret encoding. if isinstance(secret, unicode): secret = secret.encode("utf-8") # generate the mysql323 hash first (as it would be in the db MASK_32 = 0xffffffff MASK_31 = 0x7fffffff WHITE = b' \t' nr1 = 0x50305735 nr2 = 0x12345671 add = 7 for c in secret: if c in WHITE: continue tmp = byte_elem_value(c) nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32 nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32 add = (add+tmp) & MASK_32 mysql323_hash = u("%08x%08x") % (nr1 & MASK_31, nr2 & MASK_31) # NOTE: can't use digest directly, since bcrypt stops at first NULL. # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password # (XXX: citation needed), so we don't want key to be > 55 bytes. # thus, have to use base64 (44 bytes) rather than hex (64 bytes). key = b64encode(sha256(mysql323_hash).digest()) return self._calc_checksum_backend(key)
def test_norm_hash_name(self): """norm_hash_name()""" from itertools import chain from passlib.crypto.digest import norm_hash_name, _known_hash_names # snapshot warning state, ignore unknown hash warnings ctx = warnings.catch_warnings() ctx.__enter__() self.addCleanup(ctx.__exit__) warnings.filterwarnings("ignore", '.*unknown hash') # test string types self.assertEqual(norm_hash_name(u("MD4")), "md4") self.assertEqual(norm_hash_name(b"MD4"), "md4") self.assertRaises(TypeError, norm_hash_name, None) # test selected results for row in chain(_known_hash_names, self.norm_hash_samples): for idx, format in enumerate(self.norm_hash_formats): correct = row[idx] for value in row: result = norm_hash_name(value, format) self.assertEqual(result, correct, "name=%r, format=%r:" % (value, format))
def from_string(cls, hash): rounds, salt, chk = uh.parse_mc3(hash, cls.ident, sep=u("."), handler=cls) salt = unhexlify(salt.encode("ascii")) if chk: chk = unhexlify(chk.encode("ascii")) return cls(rounds=rounds, salt=salt, checksum=chk)
def from_string(cls, hash): if isinstance(hash, bytes): hash = hash.decode("ascii") if hash == u('a'): return cls(checksum=hash) else: raise ValueError
def to_string(self, withchk=True): salt = hexlify(self.salt).decode("ascii").upper() if withchk and self.checksum: chk = hexlify(self.checksum).decode("ascii").upper() else: chk = None return uh.render_mc3(self.ident, self.rounds, salt, chk, sep=u("."))
def genseed(value=None): """generate prng seed value from system resources""" from hashlib import sha512 if hasattr(value, "getstate") and hasattr(value, "getrandbits"): # caller passed in RNG as seed value try: value = value.getstate() except NotImplementedError: # this method throws error for e.g. SystemRandom instances, # so fall back to extracting 4k of state value = value.getrandbits(1 << 15) text = u("%s %s %s %.15f %.15f %s") % ( # if caller specified a seed value, mix it in value, # add current process id # NOTE: not available in some environments, e.g. GAE os.getpid() if hasattr(os, "getpid") else None, # id of a freshly created object. # (at least 1 byte of which should be hard to predict) id(object()), # the current time, to whatever precision os uses time.time(), time.clock(), # if urandom available, might as well mix some bytes in. os.urandom(32).decode("latin-1") if has_urandom else 0, ) # hash it all up and return it as int/long return int(sha512(text.encode("utf-8")).hexdigest(), 16)
def _calc_checksum(self, secret): if self.checksum: # NOTE: hash will generally be "!", but we want to preserve # it in case it's something else, like "*". return self.checksum else: return u("!")
def test_12_ident(self): # test ident is proxied h = uh.PrefixWrapper("h2", "ldap_md5", "{XXX}") self.assertEqual(h.ident, u("{XXX}{MD5}")) self.assertIs(h.ident_values, None) # test lack of ident means no proxy h = uh.PrefixWrapper("h2", "des_crypt", "{XXX}") self.assertIs(h.ident, None) self.assertIs(h.ident_values, None) # test orig_prefix disabled ident proxy h = uh.PrefixWrapper("h1", "ldap_md5", "{XXX}", "{MD5}") self.assertIs(h.ident, None) self.assertIs(h.ident_values, None) # test custom ident overrides default h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{X") self.assertEqual(h.ident, u("{X")) self.assertIs(h.ident_values, None) # test custom ident must match h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{XXX}A") self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5", "{XXX}", ident="{XY") self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5", "{XXX}", ident="{XXXX") # test ident_values is proxied h = uh.PrefixWrapper("h4", "phpass", "{XXX}") self.assertIs(h.ident, None) self.assertEqual(h.ident_values, [ u("{XXX}$P$"), u("{XXX}$H$") ]) # test ident=True means use prefix even if hash has no ident. h = uh.PrefixWrapper("h5", "des_crypt", "{XXX}", ident=True) self.assertEqual(h.ident, u("{XXX}")) self.assertIs(h.ident_values, None) # ... but requires prefix self.assertRaises(ValueError, uh.PrefixWrapper, "h6", "des_crypt", ident=True) # orig_prefix + HasManyIdent - warning with self.assertWarningList("orig_prefix.*may not work correctly"): h = uh.PrefixWrapper("h7", "phpass", orig_prefix="$", prefix="?") self.assertEqual(h.ident_values, None) # TODO: should output (u("?P$"), u("?H$"))) self.assertEqual(h.ident, None)
def _init_ldap_crypt_handlers(): #XXX: it's not nice to play in globals like this, # but don't want to write all all these handlers g = globals() for wname in unix_crypt_schemes: name = 'ldap_' + wname g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True) del g
def _init_ldap_crypt_handlers(): # NOTE: I don't like to implicitly modify globals() like this, # but don't want to write out all these handlers out either :) g = globals() for wname in unix_crypt_schemes: name = 'ldap_' + wname g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True) del g
def test_crypt(self): "test crypt.crypt() wrappers" from passlib.utils import has_crypt, safe_crypt, test_crypt # test everything is disabled if not has_crypt: self.assertEqual(safe_crypt("test", "aa"), None) self.assertFalse(test_crypt("test", "aaqPiZY5xR5l.")) raise self.skipTest("crypt.crypt() not available") # XXX: this assumes *every* crypt() implementation supports des_crypt. # if this fails for some platform, this test will need modifying. # test return type self.assertIsInstance(safe_crypt(u("test"), u("aa")), unicode) # test ascii password h1 = u('aaqPiZY5xR5l.') self.assertEqual(safe_crypt(u('test'), u('aa')), h1) self.assertEqual(safe_crypt(b('test'), b('aa')), h1) # test utf-8 / unicode password h2 = u('aahWwbrUsKZk.') self.assertEqual(safe_crypt(u('test\u1234'), 'aa'), h2) self.assertEqual(safe_crypt(b('test\xe1\x88\xb4'), 'aa'), h2) # test latin-1 password hash = safe_crypt(b('test\xff'), 'aa') if PY3: # py3 supports utf-8 bytes only. self.assertEqual(hash, None) else: # but py2 is fine. self.assertEqual(hash, u('aaOx.5nbTU/.M')) # test rejects null chars in password self.assertRaises(ValueError, safe_crypt, '\x00', 'aa') # check test_crypt() h1x = h1[:-1] + 'x' self.assertTrue(test_crypt("test", h1)) self.assertFalse(test_crypt("test", h1x)) # check crypt returning variant error indicators # some platforms return None on errors, others empty string, # The BSDs in some cases return ":" import passlib.utils as mod orig = mod._crypt try: fake = None mod._crypt = lambda secret, hash: fake for fake in [None, "", ":", ":0", "*0"]: self.assertEqual(safe_crypt("test", "aa"), None) self.assertFalse(test_crypt("test", h1)) fake = 'xxx' self.assertEqual(safe_crypt("test", "aa"), "xxx") finally: mod._crypt = orig
def test_encode_transposed_bytes(self): "test encode_transposed_bytes()" engine = self.engine for result, input, offsets in self.transposed + self.transposed_dups: tmp = engine.encode_transposed_bytes(input, offsets) out = engine.decode_bytes(tmp) self.assertEqual(out, result) self.assertRaises(TypeError, engine.encode_transposed_bytes, u("a"), [])
def test_90_checksums(self): """test internal parsing of 'checksum' keyword""" # check non-bytes checksum values are rejected self.assertRaises(TypeError, self.handler, use_defaults=True, checksum={'sha-1': u('X')*20}) # check sha-1 is required self.assertRaises(ValueError, self.handler, use_defaults=True, checksum={'sha-256': b'X'*32})
def test_07_encodings(self): "test 'encoding' kwd" # test bad encodings cause failure in constructor self.assertRaises(ValueError, apache.HtpasswdFile, encoding="utf-16") # check sample utf-8 ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding="utf-8", return_unicode=True) self.assertEqual(ht.users(), [ u("user\u00e6") ]) # test deprecated encoding=None with self.assertWarningList("``encoding=None`` is deprecated"): ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding=None) self.assertEqual(ht.users(), [ b('user\xc3\xa6') ]) # check sample latin-1 ht = apache.HtpasswdFile.from_string(self.sample_04_latin1, encoding="latin-1", return_unicode=True) self.assertEqual(ht.users(), [ u("user\u00e6") ])
class md5_crypt(uh.HasManyBackends, _MD5_Common): """This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :type salt_size: int :param salt_size: Optional number of characters to use when autogenerating new salts. Defaults to 8, but can be any value between 0 and 8. (This is mainly needed when generating Cisco-compatible hashes, which require ``salt_size=4``). :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== name = "md5_crypt" ident = u("$1$") #=================================================================== # methods #=================================================================== # FIXME: can't find definitive policy on how md5-crypt handles non-ascii. # all backends currently coerce -> utf-8 backends = ("os_crypt", "builtin") _has_backend_builtin = True @classproperty def _has_backend_os_crypt(cls): return test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/') def _calc_checksum_builtin(self, secret): return _raw_md5_crypt(secret, self.salt) def _calc_checksum_os_crypt(self, secret): config = self.ident + self.salt hash = safe_crypt(secret, config) if hash: assert hash.startswith(config) and len(hash) == len(config) + 23 return hash[-22:] else: return self._calc_checksum_builtin(secret)
def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") data = self.salt.encode("ascii") + secret + self.salt.encode("ascii") return str_to_uascii(hashlib.sha1(data).hexdigest()) #============================================================================= # test sample algorithms - really a self-test of HandlerCase #============================================================================= # TODO: provide data samples for algorithms # (positive knowns, negative knowns, invalid identify) UPASS_TEMP = u('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2') class UnsaltedHashTest(HandlerCase): handler = UnsaltedHash known_correct_hashes = [ ("password", "61cfd32684c47de231f1f982c214e884133762c0"), (UPASS_TEMP, '96b329d120b97ff81ada770042e44ba87343ad2b'), ] def test_bad_kwds(self): self.assertRaises(TypeError, UnsaltedHash, salt='x') self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1)
def test_50_norm_ident(self): """test GenericHandler + HasManyIdents""" # setup helpers class d1(uh.HasManyIdents, uh.GenericHandler): name = 'd1' setting_kwds = ('ident', ) default_ident = u("!A") ident_values = (u("!A"), u("!B")) ident_aliases = {u("A"): u("!A")} def norm_ident(**k): return d1(**k).ident # check ident=None self.assertRaises(TypeError, norm_ident) self.assertRaises(TypeError, norm_ident, ident=None) self.assertEqual(norm_ident(use_defaults=True), u('!A')) # check valid idents self.assertEqual(norm_ident(ident=u('!A')), u('!A')) self.assertEqual(norm_ident(ident=u('!B')), u('!B')) self.assertRaises(ValueError, norm_ident, ident=u('!C')) # check aliases self.assertEqual(norm_ident(ident=u('A')), u('!A')) # check invalid idents self.assertRaises(ValueError, norm_ident, ident=u('B')) # check identify is honoring ident system self.assertTrue(d1.identify(u("!Axxx"))) self.assertTrue(d1.identify(u("!Bxxx"))) self.assertFalse(d1.identify(u("!Cxxx"))) self.assertFalse(d1.identify(u("A"))) self.assertFalse(d1.identify(u(""))) self.assertRaises(TypeError, d1.identify, None) self.assertRaises(TypeError, d1.identify, 1) # check default_ident missing is detected. d1.default_ident = None self.assertRaises(AssertionError, norm_ident, use_defaults=True)
def to_string(self): hash = u("%s%s") % (self.salt, self.checksum) return uascii_to_str(hash)
class crypt16(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler): """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :param bool truncate_error: By default, crypt16 will silently truncate passwords larger than 16 bytes. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. versionadded:: 1.7 :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "crypt16" setting_kwds = ("salt", "truncate_error") #-------------------- # GenericHandler #-------------------- checksum_size = 22 checksum_chars = uh.HASH64_CHARS #-------------------- # HasSalt #-------------------- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #-------------------- # TruncateMixin #-------------------- truncate_size = 16 #=================================================================== # internal helpers #=================================================================== _hash_regex = re.compile(u(r""" ^ (?P<salt>[./a-z0-9]{2}) (?P<chk>[./a-z0-9]{22})? $"""), re.X|re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk) def to_string(self): hash = u("%s%s") % (self.salt, self.checksum) return uascii_to_str(hash) #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") # check for truncation (during .hash() calls only) if self.use_defaults: self._check_truncate_policy(secret) # parse salt value try: salt_value = h64.decode_int12(self.salt.encode("ascii")) except ValueError: # pragma: no cover - caught by class raise suppress_cause(ValueError("invalid chars in salt")) # convert first 8 byts of secret string into an integer, key1 = _crypt_secret_to_key(secret) # run data through des using input of 0 result1 = des_encrypt_int_block(key1, 0, salt_value, 20) # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars) key2 = _crypt_secret_to_key(secret[8:16]) # run data through des using input of 0 result2 = des_encrypt_int_block(key2, 0, salt_value, 5) # done chk = h64big.encode_int64(result1) + h64big.encode_int64(result2) return chk.decode("ascii")
class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler): """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 12, must be between 4 and 31, inclusive. This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}` -- increasing the rounds by +1 will double the amount of time taken. :type ident: str :param ident: Specifies which version of the BCrypt algorithm will be used when creating a new hash. Typically this option is not needed, as the default (``"2a"``) is usually the correct choice. If specified, it must be one of the following: * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore. * ``"2a"`` - latest revision of the official BCrypt algorithm, and the current default. * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, identical to ``"2a"`` in all but name. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``rounds`` that are too small or too large, and ``salt`` strings that are too long. .. versionadded:: 1.6 .. versionchanged:: 1.6 This class now supports ``"2y"`` hashes, and recognizes (but does not support) the broken ``"2x"`` hashes. (see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>` for details). .. versionchanged:: 1.6 Added a pure-python backend. """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "bcrypt" setting_kwds = ("salt", "rounds", "ident") checksum_size = 31 checksum_chars = bcrypt64.charmap #--HasManyIdents-- default_ident = u("$2a$") ident_values = (u("$2$"), IDENT_2A, IDENT_2X, IDENT_2Y) ident_aliases = {u("2"): u("$2$"), u("2a"): IDENT_2A, u("2y"): IDENT_2Y} #--HasSalt-- min_salt_size = max_salt_size = 22 salt_chars = bcrypt64.charmap # NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap #--HasRounds-- default_rounds = 12 # current passlib default min_rounds = 4 # bcrypt spec specified minimum max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds) rounds_cost = "log2" #=================================================================== # formatting #=================================================================== @classmethod def from_string(cls, hash): ident, tail = cls._parse_ident(hash) if ident == IDENT_2X: raise ValueError("crypt_blowfish's buggy '2x' hashes are not " "currently supported") rounds_str, data = tail.split(u("$")) rounds = int(rounds_str) if rounds_str != u('%02d') % (rounds,): raise uh.exc.MalformedHashError(cls, "malformed cost field") salt, chk = data[:22], data[22:] return cls( rounds=rounds, salt=salt, checksum=chk or None, ident=ident, ) def to_string(self): hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum or u('')) return uascii_to_str(hash) def _get_config(self, ident=None): "internal helper to prepare config string for backends" if ident is None: ident = self.ident if ident == IDENT_2Y: ident = IDENT_2A else: assert ident != IDENT_2X config = u("%s%02d$%s") % (ident, self.rounds, self.salt) return uascii_to_str(config) #=================================================================== # specialized salt generation - fixes passlib issue 25 #=================================================================== @classmethod def _bind_needs_update(cls, **settings): return cls._needs_update @classmethod def _needs_update(cls, hash, secret): if isinstance(hash, bytes): hash = hash.decode("ascii") # check for incorrect padding bits (passlib issue 25) if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]: return True # TODO: try to detect incorrect $2x$ hashes using *secret* return False @classmethod def normhash(cls, hash): "helper to normalize hash, correcting any bcrypt padding bits" if cls.identify(hash): return cls.from_string(hash).to_string() else: return hash def _generate_salt(self, salt_size): # override to correct generate salt bits salt = super(bcrypt, self)._generate_salt(salt_size) return bcrypt64.repair_unused(salt) def _norm_salt(self, salt, **kwds): salt = super(bcrypt, self)._norm_salt(salt, **kwds) assert salt is not None, "HasSalt didn't generate new salt!" changed, salt = bcrypt64.check_repair_unused(salt) if changed: # FIXME: if salt was provided by user, this message won't be # correct. not sure if we want to throw error, or use different warning. warn( "encountered a bcrypt salt with incorrectly set padding bits; " "you may want to use bcrypt.normhash() " "to fix this; see Passlib 1.5.3 changelog.", PasslibHashWarning) return salt def _norm_checksum(self, checksum): checksum = super(bcrypt, self)._norm_checksum(checksum) if not checksum: return None changed, checksum = bcrypt64.check_repair_unused(checksum) if changed: warn( "encountered a bcrypt hash with incorrectly set padding bits; " "you may want to use bcrypt.normhash() " "to fix this; see Passlib 1.5.3 changelog.", PasslibHashWarning) return checksum #=================================================================== # primary interface #=================================================================== backends = ("pybcrypt", "bcryptor", "os_crypt", "builtin") @classproperty def _has_backend_pybcrypt(cls): return pybcrypt_hashpw is not None @classproperty def _has_backend_bcryptor(cls): return bcryptor_engine is not None @classproperty def _has_backend_builtin(cls): if os.environ.get("PASSLIB_BUILTIN_BCRYPT") not in ["enable","enabled"]: return False # look at it cross-eyed, and it loads itself _load_builtin() return True @classproperty def _has_backend_os_crypt(cls): # XXX: what to do if only h2 is supported? h1 is *very* rare. h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju' h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty' return test_crypt("test",h1) and test_crypt("test", h2) @classmethod def _no_backends_msg(cls): return "no bcrypt backends available - please install py-bcrypt" def _calc_checksum_os_crypt(self, secret): config = self._get_config() hash = safe_crypt(secret, config) if hash: assert hash.startswith(config) and len(hash) == len(config)+31 return hash[-31:] else: # NOTE: it's unlikely any other backend will be available, # but checking before we bail, just in case. for name in self.backends: if name != "os_crypt" and self.has_backend(name): func = getattr(self, "_calc_checksum_" + name) return func(secret) raise uh.exc.MissingBackendError( "password can't be handled by os_crypt, " "recommend installing py-bcrypt.", ) def _calc_checksum_pybcrypt(self, secret): # py-bcrypt behavior: # py2: unicode secret/hash encoded as ascii bytes before use, # bytes taken as-is; returns ascii bytes. # py3: not supported (patch submitted) if isinstance(secret, unicode): secret = secret.encode("utf-8") if _BNULL in secret: raise uh.exc.NullPasswordError(self) config = self._get_config() hash = pybcrypt_hashpw(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31 return str_to_uascii(hash[-31:]) def _calc_checksum_bcryptor(self, secret): # bcryptor behavior: # py2: unicode secret/hash encoded as ascii bytes before use, # bytes taken as-is; returns ascii bytes. # py3: not supported if isinstance(secret, unicode): secret = secret.encode("utf-8") if _BNULL in secret: # NOTE: especially important to forbid NULLs for bcryptor, # since it happily accepts them, and then silently truncates # the password at first one it encounters :( raise uh.exc.NullPasswordError(self) if self.ident == IDENT_2: # bcryptor doesn't support $2$ hashes; but we can fake $2$ behavior # using the $2a$ algorithm, by repeating the password until # it's at least 72 chars in length. if secret: secret = repeat_string(secret, 72) config = self._get_config(IDENT_2A) else: config = self._get_config() hash = bcryptor_engine(False).hash_key(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31 return str_to_uascii(hash[-31:]) def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") if _BNULL in secret: raise uh.exc.NullPasswordError(self) chk = _builtin_bcrypt(secret, self.ident.strip("$"), self.salt.encode("ascii"), self.rounds) return chk.decode("ascii")
from passlib.crypto import scrypt as _scrypt from passlib.utils import h64, to_bytes from passlib.utils.binary import h64, b64s_decode, b64s_encode from passlib.utils.compat import u, bascii_to_str, suppress_cause from passlib.utils.decor import classproperty import passlib.utils.handlers as uh # local __all__ = [ "scrypt", ] #============================================================================= # scrypt format identifiers #============================================================================= IDENT_SCRYPT = u("$scrypt$") # identifier used by passlib IDENT_7 = u("$7$") # used by official scrypt spec _UDOLLAR = u("$") #============================================================================= # handler #============================================================================= class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents, uh.GenericHandler): """This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, a variable number of rounds, as well as some custom tuning parameters unique to scrypt (see below). The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
# pkg import passlib.utils.handlers as uh from passlib.utils.compat import u # local __all__ = [ "roundup_plaintext", "ldap_hex_md5", "ldap_hex_sha1", ] # ============================================================================= # # ============================================================================= roundup_plaintext = uh.PrefixWrapper("roundup_plaintext", "plaintext", prefix=u("{plaintext}"), lazy=True) # NOTE: these are here because they're currently only known to be used by roundup ldap_hex_md5 = uh.PrefixWrapper("ldap_hex_md5", "hex_md5", u("{MD5}"), lazy=True) ldap_hex_sha1 = uh.PrefixWrapper("ldap_hex_sha1", "hex_sha1", u("{SHA}"), lazy=True) # ============================================================================= # eof # =============================================================================
class oracle11(uh.HasSalt, uh.GenericHandler): """This class implements the Oracle11g password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 20 hexadecimal characters. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.6 """ # =================================================================== # class attrs # =================================================================== # --GenericHandler-- name = "oracle11" setting_kwds = ("salt", ) checksum_size = 40 checksum_chars = uh.UPPER_HEX_CHARS # --HasSalt-- min_salt_size = max_salt_size = 20 salt_chars = uh.UPPER_HEX_CHARS # =================================================================== # methods # =================================================================== _hash_regex = re.compile( u("^S:(?P<chk>[0-9a-f]{40})(?P<salt>[0-9a-f]{20})$"), re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk.upper()) def to_string(self): chk = self.checksum hash = u("S:%s%s") % (chk.upper(), self.salt.upper()) return uascii_to_str(hash) def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") chk = sha1(secret + unhexlify(self.salt.encode("ascii"))).hexdigest() return str_to_uascii(chk).upper()
def to_string(self): chk = self.checksum hash = u("S:%s%s") % (chk.upper(), self.salt.upper()) return uascii_to_str(hash)
def to_string(self): hash = u("%s%s%s%s") % (self.ident, h64.encode_int6(self.rounds).decode("ascii"), self.salt, self.checksum or u('')) return uascii_to_str(hash)
class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """This class provides a format for storing SCRAM passwords, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: bytes :param salt: Optional salt bytes. If specified, the length must be between 0-1024 bytes. If not specified, a 12 byte salt will be autogenerated (this is recommended). :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 12 bytes, but can be any value between 0 and 1024. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 100000, but must be within ``range(1,1<<32)``. :type algs: list of strings :param algs: Specify list of digest algorithms to use. By default each scram hash will contain digests for SHA-1, SHA-256, and SHA-512. This can be overridden by specify either be a list such as ``["sha-1", "sha-256"]``, or a comma-separated string such as ``"sha-1, sha-256"``. Names are case insensitive, and may use :mod:`!hashlib` or `IANA <http://www.iana.org/assignments/hash-function-text-names>`_ hash names. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``rounds`` that are too small or too large, and ``salt`` strings that are too long. .. versionadded:: 1.6 In addition to the standard :ref:`password-hash-api` methods, this class also provides the following methods for manipulating Passlib scram hashes in ways useful for pluging into a SCRAM protocol stack: .. automethod:: extract_digest_info .. automethod:: extract_digest_algs .. automethod:: derive_digest """ # =================================================================== # class attrs # =================================================================== # NOTE: unlike most GenericHandler classes, the 'checksum' attr of # ScramHandler is actually a map from digest_name -> digest, so # many of the standard methods have been overridden. # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide # a sanity check; the underlying pbkdf2 specifies no bounds for either. # --GenericHandler-- name = "scram" setting_kwds = ("salt", "salt_size", "rounds", "algs") ident = u("$scram$") # --HasSalt-- default_salt_size = 12 max_salt_size = 1024 # --HasRounds-- default_rounds = 100000 min_rounds = 1 max_rounds = 2**32 - 1 rounds_cost = "linear" # --custom-- # default algorithms when creating new hashes. default_algs = ["sha-1", "sha-256", "sha-512"] # list of algs verify prefers to use, in order. _verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"] # =================================================================== # instance attrs # =================================================================== # 'checksum' is different from most GenericHandler subclasses, # in that it contains a dict mapping from alg -> digest, # or None if no checksum present. # list of algorithms to create/compare digests for. algs = None # =================================================================== # scram frontend helpers # =================================================================== @classmethod def extract_digest_info(cls, hash, alg): """return (salt, rounds, digest) for specific hash algorithm. :type hash: str :arg hash: :class:`!scram` hash stored for desired user :type alg: str :arg alg: Name of digest algorithm (e.g. ``"sha-1"``) requested by client. This value is run through :func:`~passlib.crypto.digest.norm_hash_name`, so it is case-insensitive, and can be the raw SCRAM mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name, or the hashlib name. :raises KeyError: If the hash does not contain an entry for the requested digest algorithm. :returns: A tuple containing ``(salt, rounds, digest)``, where *digest* matches the raw bytes returned by SCRAM's :func:`Hi` function for the stored password, the provided *salt*, and the iteration count (*rounds*). *salt* and *digest* are both raw (unencoded) bytes. """ # XXX: this could be sped up by writing custom parsing routine # that just picks out relevant digest, and doesn't bother # with full structure validation each time it's called. alg = norm_hash_name(alg, "iana") self = cls.from_string(hash) chkmap = self.checksum if not chkmap: raise ValueError("scram hash contains no digests") return self.salt, self.rounds, chkmap[alg] @classmethod def extract_digest_algs(cls, hash, format="iana"): """Return names of all algorithms stored in a given hash. :type hash: str :arg hash: The :class:`!scram` hash to parse :type format: str :param format: This changes the naming convention used by the returned algorithm names. By default the names are IANA-compatible; possible values are ``"iana"`` or ``"hashlib"``. :returns: Returns a list of digest algorithms; e.g. ``["sha-1"]`` """ # XXX: this could be sped up by writing custom parsing routine # that just picks out relevant names, and doesn't bother # with full structure validation each time it's called. algs = cls.from_string(hash).algs if format == "iana": return algs else: return [norm_hash_name(alg, format) for alg in algs] @classmethod def derive_digest(cls, password, salt, rounds, alg): """helper to create SaltedPassword digest for SCRAM. This performs the step in the SCRAM protocol described as:: SaltedPassword := Hi(Normalize(password), salt, i) :type password: unicode or utf-8 bytes :arg password: password to run through digest :type salt: bytes :arg salt: raw salt data :type rounds: int :arg rounds: number of iterations. :type alg: str :arg alg: name of digest to use (e.g. ``"sha-1"``). :returns: raw bytes of ``SaltedPassword`` """ if isinstance(password, bytes): password = password.decode("utf-8") # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8, # and handle normalizing alg name. return pbkdf2_hmac(alg, saslprep(password), salt, rounds) # =================================================================== # serialization # =================================================================== @classmethod def from_string(cls, hash): hash = to_native_str(hash, "ascii", "hash") if not hash.startswith("$scram$"): raise uh.exc.InvalidHashError(cls) parts = hash[7:].split("$") if len(parts) != 3: raise uh.exc.MalformedHashError(cls) rounds_str, salt_str, chk_str = parts # decode rounds rounds = int(rounds_str) if rounds_str != str(rounds): # forbid zero padding, etc. raise uh.exc.MalformedHashError(cls) # decode salt try: salt = ab64_decode(salt_str.encode("ascii")) except TypeError: raise uh.exc.MalformedHashError(cls) # decode algs/digest list if not chk_str: # scram hashes MUST have something here. raise uh.exc.MalformedHashError(cls) elif "=" in chk_str: # comma-separated list of 'alg=digest' pairs algs = None chkmap = {} for pair in chk_str.split(","): alg, digest = pair.split("=") try: chkmap[alg] = ab64_decode(digest.encode("ascii")) except TypeError: raise uh.exc.MalformedHashError(cls) else: # comma-separated list of alg names, no digests algs = chk_str chkmap = None # return new object return cls(rounds=rounds, salt=salt, checksum=chkmap, algs=algs) def to_string(self): salt = bascii_to_str(ab64_encode(self.salt)) chkmap = self.checksum chk_str = ",".join("%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg]))) for alg in self.algs) return "$scram$%d$%s$%s" % (self.rounds, salt, chk_str) # =================================================================== # variant constructor # =================================================================== @classmethod def using(cls, default_algs=None, algs=None, **kwds): # parse aliases if algs is not None: assert default_algs is None default_algs = algs # create subclass subcls = super(scram, cls).using(**kwds) # fill in algs if default_algs is not None: subcls.default_algs = cls._norm_algs(default_algs) return subcls # =================================================================== # init # =================================================================== def __init__(self, algs=None, **kwds): super(scram, self).__init__(**kwds) # init algs digest_map = self.checksum if algs is not None: if digest_map is not None: raise RuntimeError( "checksum & algs kwds are mutually exclusive") algs = self._norm_algs(algs) elif digest_map is not None: # derive algs list from digest map (if present). algs = self._norm_algs(digest_map.keys()) elif self.use_defaults: algs = list(self.default_algs) assert self._norm_algs( algs) == algs, "invalid default algs: %r" % (algs, ) else: raise TypeError("no algs list specified") self.algs = algs def _norm_checksum(self, checksum, relaxed=False): if not isinstance(checksum, dict): raise uh.exc.ExpectedTypeError(checksum, "dict", "checksum") for alg, digest in iteritems(checksum): if alg != norm_hash_name(alg, "iana"): raise ValueError("malformed algorithm name in scram hash: %r" % (alg, )) if len(alg) > 9: raise ValueError("SCRAM limits algorithm names to " "9 characters: %r" % (alg, )) if not isinstance(digest, bytes): raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests") # TODO: verify digest size (if digest is known) if "sha-1" not in checksum: # NOTE: required because of SCRAM spec. raise ValueError("sha-1 must be in algorithm list of scram hash") return checksum @classmethod def _norm_algs(cls, algs): """normalize algs parameter""" if isinstance(algs, native_string_types): algs = splitcomma(algs) algs = sorted(norm_hash_name(alg, "iana") for alg in algs) if any(len(alg) > 9 for alg in algs): raise ValueError("SCRAM limits alg names to max of 9 characters") if "sha-1" not in algs: # NOTE: required because of SCRAM spec (rfc 5802) raise ValueError("sha-1 must be in algorithm list of scram hash") return algs # =================================================================== # migration # =================================================================== def _calc_needs_update(self, **kwds): # marks hashes as deprecated if they don't include at least all default_algs. # XXX: should we deprecate if they aren't exactly the same, # to permit removing legacy hashes? if not set(self.algs).issuperset(self.default_algs): return True # hand off to base implementation return super(scram, self)._calc_needs_update(**kwds) # =================================================================== # digest methods # =================================================================== def _calc_checksum(self, secret, alg=None): rounds = self.rounds salt = self.salt hash = self.derive_digest if alg: # if requested, generate digest for specific alg return hash(secret, salt, rounds, alg) else: # by default, return dict containing digests for all algs return dict( (alg, hash(secret, salt, rounds, alg)) for alg in self.algs) @classmethod def verify(cls, secret, hash, full=False): uh.validate_secret(secret) self = cls.from_string(hash) chkmap = self.checksum if not chkmap: raise ValueError("expected %s hash, got %s config string instead" % (cls.name, cls.name)) # NOTE: to make the verify method efficient, we just calculate hash # of shortest digest by default. apps can pass in "full=True" to # check entire hash for consistency. if full: correct = failed = False for alg, digest in iteritems(chkmap): other = self._calc_checksum(secret, alg) # NOTE: could do this length check in norm_algs(), # but don't need to be that strict, and want to be able # to parse hashes containing algs not supported by platform. # it's fine if we fail here though. if len(digest) != len(other): raise ValueError( "mis-sized %s digest in scram hash: %r != %r" % (alg, len(digest), len(other))) if consteq(other, digest): correct = True else: failed = True if correct and failed: raise ValueError("scram hash verified inconsistently, " "may be corrupted") else: return correct else: # XXX: should this just always use sha1 hash? would be faster. # otherwise only verify against one hash, pick one w/ best security. for alg in self._verify_algs: if alg in chkmap: other = self._calc_checksum(secret, alg) return consteq(other, chkmap[alg]) # there should always be sha-1 at the very least, # or something went wrong inside _norm_algs() raise AssertionError("sha-1 digest not found!")
class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """This class implements the FSHP password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :param salt: Optional raw salt string. If not specified, one will be autogenerated (this is recommended). :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 16 bytes, but can be any non-negative value. :param rounds: Optional number of rounds to use. Defaults to 50000, must be between 1 and 4294967295, inclusive. :param variant: Optionally specifies variant of FSHP to use. * ``0`` - uses SHA-1 digest (deprecated). * ``1`` - uses SHA-2/256 digest (default). * ``2`` - uses SHA-2/384 digest. * ``3`` - uses SHA-2/512 digest. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``rounds`` that are too small or too large, and ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "fshp" setting_kwds = ("salt", "salt_size", "rounds", "variant") checksum_chars = uh.PADDED_BASE64_CHARS ident = u("{FSHP") # checksum_size is property() that depends on variant #--HasRawSalt-- default_salt_size = 16 # current passlib default, FSHP uses 8 min_salt_size = 0 max_salt_size = None #--HasRounds-- # FIXME: should probably use different default rounds # based on the variant. setting for default variant (sha256) for now. default_rounds = 50000 # current passlib default, FSHP uses 4096 min_rounds = 1 # set by FSHP max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP rounds_cost = "linear" #--variants-- default_variant = 1 _variant_info = { # variant: (hash name, digest size) 0: ("sha1", 20), 1: ("sha256", 32), 2: ("sha384", 48), 3: ("sha512", 64), } _variant_aliases = dict([(unicode(k), k) for k in _variant_info] + [(v[0], k) for k, v in iteritems(_variant_info)]) #=================================================================== # instance attrs #=================================================================== variant = None #=================================================================== # init #=================================================================== def __init__(self, variant=None, **kwds): # NOTE: variant must be set first, since it controls checksum size, etc. self.use_defaults = kwds.get("use_defaults") # load this early self.variant = self._norm_variant(variant) super(fshp, self).__init__(**kwds) def _norm_variant(self, variant): if variant is None: if not self.use_defaults: raise TypeError("no variant specified") variant = self.default_variant if isinstance(variant, bytes): variant = variant.decode("ascii") if isinstance(variant, unicode): try: variant = self._variant_aliases[variant] except KeyError: raise ValueError("invalid fshp variant") if not isinstance(variant, int): raise TypeError("fshp variant must be int or known alias") if variant not in self._variant_info: raise ValueError("invalid fshp variant") return variant @property def checksum_alg(self): return self._variant_info[self.variant][0] @property def checksum_size(self): return self._variant_info[self.variant][1] #=================================================================== # formatting #=================================================================== _hash_regex = re.compile( u(r""" ^ \{FSHP (\d+)\| # variant (\d+)\| # salt size (\d+)\} # rounds ([a-zA-Z0-9+/]+={0,3}) # digest $"""), re.X) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) variant, salt_size, rounds, data = m.group(1, 2, 3, 4) variant = int(variant) salt_size = int(salt_size) rounds = int(rounds) try: data = b64decode(data.encode("ascii")) except TypeError: raise uh.exc.MalformedHashError(cls) salt = data[:salt_size] chk = data[salt_size:] return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant) @property def _stub_checksum(self): return b('\x00') * self.checksum_size def to_string(self): chk = self.checksum or self._stub_checksum salt = self.salt data = bascii_to_str(b64encode(salt + chk)) return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data) #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") # NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed. # this has only a minimal impact on security, # but it is worth noting this deviation. return pbkdf1( secret=self.salt, salt=secret, rounds=self.rounds, keylen=self.checksum_size, hash=self.checksum_alg, )
def to_string(self): hash = u("@salt%s%s") % (self.salt, self.checksum or self._stub_checksum) return uascii_to_str(hash)
def test_91_parsehash(self): """test parsehash()""" # NOTE: this just tests some existing GenericHandler classes from passlib import hash # # parsehash() # # simple hash w/ salt result = hash.des_crypt.parsehash("OgAwTx2l6NADI") self.assertEqual(result, { 'checksum': u('AwTx2l6NADI'), 'salt': u('Og') }) # parse rounds and extra implicit_rounds flag h = '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9' s = u('LKO/Ute40T3FNF95') c = u('U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9') result = hash.sha256_crypt.parsehash(h) self.assertEqual( result, dict(salt=s, rounds=5000, implicit_rounds=True, checksum=c)) # omit checksum result = hash.sha256_crypt.parsehash(h, checksum=False) self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True)) # sanitize result = hash.sha256_crypt.parsehash(h, sanitize=True) self.assertEqual( result, dict(rounds=5000, implicit_rounds=True, salt=u('LK**************'), checksum=u('U0pr***************************************'))) # parse w/o implicit rounds flag result = hash.sha256_crypt.parsehash( '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3' ) self.assertEqual( result, dict( checksum=u('YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'), salt=u('uy/jIAhCetNCTtb0'), rounds=10428, )) # parsing of raw checksums & salts h1 = '$pbkdf2$60000$DoEwpvQeA8B4T.k951yLUQ$O26Y3/NJEiLCVaOVPxGXshyjW8k' result = hash.pbkdf2_sha1.parsehash(h1) self.assertEqual( result, dict( checksum= b';n\x98\xdf\xf3I\x12"\xc2U\xa3\x95?\x11\x97\xb2\x1c\xa3[\xc9', rounds=60000, salt=b'\x0e\x810\xa6\xf4\x1e\x03\xc0xO\xe9=\xe7\\\x8bQ', )) # sanitizing of raw checksums & salts result = hash.pbkdf2_sha1.parsehash(h1, sanitize=True) self.assertEqual( result, dict( checksum=u('O26************************'), rounds=60000, salt=u('Do********************'), ))
class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 5001, must be between 1 and 16777215, inclusive. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``rounds`` that are too small or too large, and ``salt`` strings that are too long. .. versionadded:: 1.6 .. versionchanged:: 1.6 :meth:`hash` will now issue a warning if an even number of rounds is used (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys). """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "bsdi_crypt" setting_kwds = ("salt", "rounds") checksum_size = 11 checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 4 salt_chars = uh.HASH64_CHARS #--HasRounds-- default_rounds = 5001 min_rounds = 1 max_rounds = 16777215 # (1<<24)-1 rounds_cost = "linear" # NOTE: OpenBSD auth.conf reports 7250 as minimum allowed rounds, # but that seems to be an OS policy, not a algorithm limitation. #=================================================================== # parsing #=================================================================== _hash_regex = re.compile(u(r""" ^ _ (?P<rounds>[./a-z0-9]{4}) (?P<salt>[./a-z0-9]{4}) (?P<chk>[./a-z0-9]{11})? $"""), re.X|re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) rounds, salt, chk = m.group("rounds", "salt", "chk") return cls( rounds=h64.decode_int24(rounds.encode("ascii")), salt=salt, checksum=chk, ) def to_string(self): hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"), self.salt, self.checksum) return uascii_to_str(hash) #=================================================================== # validation #=================================================================== # NOTE: keeping this flag for admin/choose_rounds.py script. # want to eventually expose rounds logic to that script in better way. _avoid_even_rounds = True @classmethod def using(cls, **kwds): subcls = super(bsdi_crypt, cls).using(**kwds) if not subcls.default_rounds & 1: # issue warning if caller set an even 'rounds' value. warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys", uh.exc.PasslibSecurityWarning) return subcls @classmethod def _generate_rounds(cls): rounds = super(bsdi_crypt, cls)._generate_rounds() # ensure autogenerated rounds are always odd # NOTE: doing this even for default_rounds so needs_update() doesn't get # caught in a loop. # FIXME: this technically might generate a rounds value 1 larger # than the requested upper bound - but better to err on side of safety. return rounds|1 #=================================================================== # migration #=================================================================== def _calc_needs_update(self, **kwds): # mark bsdi_crypt hashes as deprecated if they have even rounds. if not self.rounds & 1: return True # hand off to base implementation return super(bsdi_crypt, self)._calc_needs_update(**kwds) #=================================================================== # backends #=================================================================== backends = ("os_crypt", "builtin") #--------------------------------------------------------------- # os_crypt backend #--------------------------------------------------------------- @classmethod def _load_backend_os_crypt(cls): if test_crypt("test", '_/...lLDAxARksGCHin.'): cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) return True else: return False def _calc_checksum_os_crypt(self, secret): config = self.to_string() hash = safe_crypt(secret, config) if hash: assert hash.startswith(config[:9]) and len(hash) == 20 return hash[-11:] else: # py3's crypt.crypt() can't handle non-utf8 bytes. # fallback to builtin alg, which is always available. return self._calc_checksum_builtin(secret) #--------------------------------------------------------------- # builtin backend #--------------------------------------------------------------- @classmethod def _load_backend_builtin(cls): cls._set_calc_checksum_backend(cls._calc_checksum_builtin) return True def _calc_checksum_builtin(self, secret): return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
class d1(uh.HasManyIdents, uh.GenericHandler): name = 'd1' setting_kwds = ('ident', ) default_ident = u("!A") ident_values = (u("!A"), u("!B")) ident_aliases = {u("A"): u("!A")}
"bcrypt", "md5_crypt", # "bsd_nthash", "bsdi_crypt", "des_crypt", ] # list of rounds_cost constants rounds_cost_values = ["linear", "log2"] # legacy import, will be removed in 1.8 from passlib.exc import MissingBackendError # internal helpers _BEMPTY = b'' _UEMPTY = u("") _USPACE = u(" ") # maximum password size which passlib will allow; see exc.PasswordSizeError MAX_PASSWORD_SIZE = int(os.environ.get("PASSLIB_MAX_PASSWORD_SIZE") or 4096) #============================================================================= # type helpers #============================================================================= class SequenceMixin(object): """ helper which lets result object act like a fixed-length sequence. subclass just needs to provide :meth:`_as_tuple()`. """
class bigcrypt(uh.HasSalt, uh.GenericHandler): """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "bigcrypt" setting_kwds = ("salt",) checksum_chars = uh.HASH64_CHARS # NOTE: checksum chars must be multiple of 11 #--HasSalt-- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #=================================================================== # internal helpers #=================================================================== _hash_regex = re.compile(u(r""" ^ (?P<salt>[./a-z0-9]{2}) (?P<chk>([./a-z0-9]{11})+)? $"""), re.X|re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk) def to_string(self): hash = u("%s%s") % (self.salt, self.checksum) return uascii_to_str(hash) def _norm_checksum(self, checksum, relaxed=False): checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed) if len(checksum) % 11: raise uh.exc.InvalidHashError(self) return checksum #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") chk = _raw_des_crypt(secret, self.salt.encode("ascii")) idx = 8 end = len(secret) while idx < end: next = idx + 8 chk += _raw_des_crypt(secret[idx:next], chk[-11:-9]) idx = next return chk.decode("ascii")
class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the Sun-MD5-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, a salt will be autogenerated (this is recommended). If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``. :type salt_size: int :param salt_size: If no salt is specified, this parameter can be used to specify the size (in characters) of the autogenerated salt. It currently defaults to 8. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 5000, must be between 0 and 4294963199, inclusive. :type bare_salt: bool :param bare_salt: Optional flag used to enable an alternate salt digest behavior used by some hash strings in this scheme. This flag can be ignored by most users. Defaults to ``False``. (see :ref:`smc-bare-salt` for details). :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``rounds`` that are too small or too large, and ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== name = "sun_md5_crypt" setting_kwds = ("salt", "rounds", "bare_salt", "salt_size") checksum_chars = uh.HASH64_CHARS checksum_size = 22 # NOTE: docs say max password length is 255. # release 9u2 # NOTE: not sure if original crypt has a salt size limit, # all instances that have been seen use 8 chars. default_salt_size = 8 min_salt_size = 0 max_salt_size = None salt_chars = uh.HASH64_CHARS default_rounds = 5000 # current passlib default min_rounds = 0 max_rounds = 4294963199 ##2**32-1-4096 # XXX: ^ not sure what it does if past this bound... does 32 int roll over? rounds_cost = "linear" ident_values = (u("$md5$"), u("$md5,")) #=================================================================== # instance attrs #=================================================================== bare_salt = False # flag to indicate legacy hashes that lack "$$" suffix #=================================================================== # constructor #=================================================================== def __init__(self, bare_salt=False, **kwds): self.bare_salt = bare_salt super(sun_md5_crypt, self).__init__(**kwds) #=================================================================== # internal helpers #=================================================================== @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) return hash.startswith(cls.ident_values) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") # # 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(u("$md5$")): rounds = 0 salt_idx = 5 elif hash.startswith(u("$md5,rounds=")): idx = hash.find(u("$"), 12) if idx == -1: raise uh.exc.MalformedHashError(cls, "unexpected end of rounds") rstr = hash[12:idx] try: rounds = int(rstr) except ValueError: raise uh.exc.MalformedHashError(cls, "bad rounds") if rstr != unicode(rounds): raise uh.exc.ZeroPaddedRoundsError(cls) if rounds == 0: # NOTE: not sure if this is forbidden by spec or not; # but allowing it would complicate things, # and it should never occur anyways. raise uh.exc.MalformedHashError(cls, "explicit zero rounds") salt_idx = idx + 1 else: raise uh.exc.InvalidHashError(cls) # # salt/checksum separation is kinda weird, # to deal cleanly with some backward-compatible workarounds # implemented by original implementation. # 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] == u("$"): raise uh.exc.MalformedHashError(cls, "too many '$' separators") # $-config for $$-hash salt = hash[salt_idx:-1] chk = None bare_salt = False elif chk_idx > 0 and hash[chk_idx - 1] == u("$"): # $$-hash salt = hash[salt_idx:chk_idx - 1] chk = hash[chk_idx + 1:] bare_salt = False else: # $-hash salt = hash[salt_idx:chk_idx] chk = hash[chk_idx + 1:] bare_salt = True return cls( rounds=rounds, salt=salt, checksum=chk, bare_salt=bare_salt, ) def to_string(self, withchk=True): ss = u('') if self.bare_salt else u('$') rounds = self.rounds if rounds > 0: hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss) else: hash = u("$md5$%s%s") % (self.salt, ss) if withchk: chk = self.checksum if chk: hash = u("%s$%s") % (hash, chk) return uascii_to_str(hash) #=================================================================== # primary interface #=================================================================== # TODO: if we're on solaris, check for native crypt() support. # this will require extra testing, to make sure native crypt # actually behaves correctly. of particular importance: # when using ""-config, make sure to append "$x" to string. def _calc_checksum(self, secret): # NOTE: no reference for how sun_md5_crypt handles unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") config = str_to_bascii(self.to_string(withchk=False)) return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii")
def to_string(self): hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"), self.salt, self.checksum) return uascii_to_str(hash)
def _calc_checksum(self, secret): return u('b') if self.flag else u('a')
class d1(uh.GenericHandler): name = 'd1' checksum_size = 4 checksum_chars = u('xz')
"mssql2005", ] # ============================================================================= # mssql 2000 # ============================================================================= def _raw_mssql(secret, salt): assert isinstance(secret, unicode) assert isinstance(salt, bytes) return sha1(secret.encode("utf-16-le") + salt).digest() BIDENT = b"0x0100" ##BIDENT2 = b("\x01\x00") UIDENT = u("0x0100") def _ident_mssql(hash, csize, bsize): """common identify for mssql 2000/2005""" if isinstance(hash, unicode): if len(hash) == csize and hash.startswith(UIDENT): return True elif isinstance(hash, bytes): if len(hash) == csize and hash.startswith(BIDENT): return True ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes ## return True else: raise uh.exc.ExpectedStringError(hash, "hash") return False
def to_string(self): hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum or u('')) return uascii_to_str(hash)
class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the PHPass Portable Hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 17, must be between 7 and 30, inclusive. This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`. :type ident: str :param ident: phpBB3 uses ``H`` instead of ``P`` for it's identifier, this may be set to ``H`` in order to generate phpBB3 compatible hashes. it defaults to ``P``. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``rounds`` that are too small or too large, and ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "phpass" setting_kwds = ("salt", "rounds", "ident") checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 8 salt_chars = uh.HASH64_CHARS #--HasRounds-- default_rounds = 17 min_rounds = 7 max_rounds = 30 rounds_cost = "log2" #--HasManyIdents-- default_ident = u("$P$") ident_values = [u("$P$"), u("$H$")] ident_aliases = {u("P"):u("$P$"), u("H"):u("$H$")} #=================================================================== # formatting #=================================================================== #$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0 # $P$ # 9 # IQRaTwmf # eRo7ud9Fh4E2PdI0S3r.L0 @classmethod def from_string(cls, hash): ident, data = cls._parse_ident(hash) rounds, salt, chk = data[0], data[1:9], data[9:] return cls( ident=ident, rounds=h64.decode_int6(rounds.encode("ascii")), salt=salt, checksum=chk or None, ) def to_string(self): hash = u("%s%s%s%s") % (self.ident, h64.encode_int6(self.rounds).decode("ascii"), self.salt, self.checksum or u('')) return uascii_to_str(hash) #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): # FIXME: can't find definitive policy on how phpass handles non-ascii. if isinstance(secret, unicode): secret = secret.encode("utf-8") real_rounds = 1<<self.rounds result = md5(self.salt.encode("ascii") + secret).digest() r = 0 while r < real_rounds: result = md5(result + secret).digest() r += 1 return h64.encode_bytes(result).decode("ascii")
# local __all__ = [ "bcrypt", ] #============================================================================= # support funcs & constants #============================================================================= _builtin_bcrypt = None def _load_builtin(): global _builtin_bcrypt if _builtin_bcrypt is None: from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt IDENT_2 = u("$2$") IDENT_2A = u("$2a$") IDENT_2X = u("$2x$") IDENT_2Y = u("$2y$") _BNULL = b('\x00') #============================================================================= # handler #============================================================================= class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler): """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") # # 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(u("$md5$")): rounds = 0 salt_idx = 5 elif hash.startswith(u("$md5,rounds=")): idx = hash.find(u("$"), 12) if idx == -1: raise uh.exc.MalformedHashError(cls, "unexpected end of rounds") rstr = hash[12:idx] try: rounds = int(rstr) except ValueError: raise uh.exc.MalformedHashError(cls, "bad rounds") if rstr != unicode(rounds): raise uh.exc.ZeroPaddedRoundsError(cls) if rounds == 0: # NOTE: not sure if this is forbidden by spec or not; # but allowing it would complicate things, # and it should never occur anyways. raise uh.exc.MalformedHashError(cls, "explicit zero rounds") salt_idx = idx + 1 else: raise uh.exc.InvalidHashError(cls) # # salt/checksum separation is kinda weird, # to deal cleanly with some backward-compatible workarounds # implemented by original implementation. # 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] == u("$"): raise uh.exc.MalformedHashError(cls, "too many '$' separators") # $-config for $$-hash salt = hash[salt_idx:-1] chk = None bare_salt = False elif chk_idx > 0 and hash[chk_idx - 1] == u("$"): # $$-hash salt = hash[salt_idx:chk_idx - 1] chk = hash[chk_idx + 1:] bare_salt = False else: # $-hash salt = hash[salt_idx:chk_idx] chk = hash[chk_idx + 1:] bare_salt = True return cls( rounds=rounds, salt=salt, checksum=chk, bare_salt=bare_salt, )
class cisco_type7(uh.GenericHandler): """ This class implements the "Type 7" password encoding used by Cisco IOS, and follows the :ref:`password-hash-api`. It has a simple 4-5 bit salt, but is nonetheless a reversible encoding instead of a real hash. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: int :param salt: This may be an optional salt integer drawn from ``range(0,16)``. If omitted, one will be chosen at random. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` values that are out of range. Note that while this class outputs digests in upper-case hexadecimal, it will accept lower-case as well. This class also provides the following additional method: .. automethod:: decode """ # =================================================================== # class attrs # =================================================================== # -------------------- # PasswordHash # -------------------- name = "cisco_type7" setting_kwds = ("salt", ) # -------------------- # GenericHandler # -------------------- checksum_chars = uh.UPPER_HEX_CHARS # -------------------- # HasSalt # -------------------- # NOTE: encoding could handle max_salt_value=99, but since key is only 52 # chars in size, not sure what appropriate behavior is for that edge case. min_salt_value = 0 max_salt_value = 52 # =================================================================== # methods # =================================================================== @classmethod def using(cls, salt=None, **kwds): subcls = super(cisco_type7, cls).using(**kwds) if salt is not None: salt = subcls._norm_salt(salt, relaxed=kwds.get("relaxed")) subcls._generate_salt = staticmethod(lambda: salt) return subcls @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") if len(hash) < 2: raise uh.exc.InvalidHashError(cls) salt = int(hash[:2]) # may throw ValueError return cls(salt=salt, checksum=hash[2:].upper()) def __init__(self, salt=None, **kwds): super(cisco_type7, self).__init__(**kwds) if salt is not None: salt = self._norm_salt(salt) elif self.use_defaults: salt = self._generate_salt() assert self._norm_salt( salt) == salt, "generated invalid salt: %r" % (salt, ) else: raise TypeError("no salt specified") self.salt = salt @classmethod def _norm_salt(cls, salt, relaxed=False): """ validate & normalize salt value. .. note:: the salt for this algorithm is an integer 0-52, not a string """ if not isinstance(salt, int): raise uh.exc.ExpectedTypeError(salt, "integer", "salt") if 0 <= salt <= cls.max_salt_value: return salt msg = "salt/offset must be in 0..52 range" if relaxed: warn(msg, uh.PasslibHashWarning) return 0 if salt < 0 else cls.max_salt_value else: raise ValueError(msg) @staticmethod def _generate_salt(): return uh.rng.randint(0, 15) def to_string(self): return "%02d%s" % (self.salt, uascii_to_str(self.checksum)) def _calc_checksum(self, secret): # XXX: no idea what unicode policy is, but all examples are # 7-bit ascii compatible, so using UTF-8 if isinstance(secret, unicode): secret = secret.encode("utf-8") return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper() @classmethod def decode(cls, hash, encoding="utf-8"): """decode hash, returning original password. :arg hash: encoded password :param encoding: optional encoding to use (defaults to ``UTF-8``). :returns: password as unicode """ self = cls.from_string(hash) tmp = unhexlify(self.checksum.encode("ascii")) raw = self._cipher(tmp, self.salt) return raw.decode(encoding) if encoding else raw # type7 uses a xor-based vingere variant, using the following secret key: _key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87") @classmethod def _cipher(cls, data, salt): """xor static key against data - encrypts & decrypts""" key = cls._key key_size = len(key) return join_byte_values( value ^ ord(key[(salt + idx) % key_size]) for idx, value in enumerate(iter_byte_values(data)))
class des_crypt(uh.TruncateMixin, uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :param bool truncate_error: By default, des_crypt will silently truncate passwords larger than 8 bytes. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. versionadded:: 1.7 :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "des_crypt" setting_kwds = ("salt", "truncate_error") #-------------------- # GenericHandler #-------------------- checksum_chars = uh.HASH64_CHARS checksum_size = 11 #-------------------- # HasSalt #-------------------- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #-------------------- # TruncateMixin #-------------------- truncate_size = 8 #=================================================================== # formatting #=================================================================== # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum _hash_regex = re.compile(u(r""" ^ (?P<salt>[./a-z0-9]{2}) (?P<chk>[./a-z0-9]{11})? $"""), re.X|re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") salt, chk = hash[:2], hash[2:] return cls(salt=salt, checksum=chk or None) def to_string(self): hash = u("%s%s") % (self.salt, self.checksum) return uascii_to_str(hash) #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): # check for truncation (during .hash() calls only) if self.use_defaults: self._check_truncate_policy(secret) return self._calc_checksum_backend(secret) #=================================================================== # backend #=================================================================== backends = ("os_crypt", "builtin") #--------------------------------------------------------------- # os_crypt backend #--------------------------------------------------------------- @classmethod def _load_backend_os_crypt(cls): if test_crypt("test", 'abgOeLfPimXQo'): cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) return True else: return False def _calc_checksum_os_crypt(self, secret): # NOTE: we let safe_crypt() encode unicode secret -> utf8; # no official policy since des-crypt predates unicode hash = safe_crypt(secret, self.salt) if hash: assert hash.startswith(self.salt) and len(hash) == 13 return hash[2:] else: # py3's crypt.crypt() can't handle non-utf8 bytes. # fallback to builtin alg, which is always available. return self._calc_checksum_builtin(secret) #--------------------------------------------------------------- # builtin backend #--------------------------------------------------------------- @classmethod def _load_backend_builtin(cls): cls._set_calc_checksum_backend(cls._calc_checksum_builtin) return True def _calc_checksum_builtin(self, secret): return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")