Beispiel #1
0
    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
Beispiel #2
0
    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')
Beispiel #3
0
    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"))
Beispiel #4
0
    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)
Beispiel #5
0
 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")))
Beispiel #6
0
    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*************************************'),
             })
Beispiel #7
0
 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)
Beispiel #8
0
 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)
Beispiel #11
0
 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)
Beispiel #12
0
    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)
Beispiel #13
0
    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********************'),
        ))
Beispiel #14
0
 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)
Beispiel #15
0
    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))
Beispiel #17
0
    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)
Beispiel #18
0
    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))
Beispiel #19
0
 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)
Beispiel #20
0
 def from_string(cls, hash):
     if isinstance(hash, bytes):
         hash = hash.decode("ascii")
     if hash == u('a'):
         return cls(checksum=hash)
     else:
         raise ValueError
Beispiel #21
0
 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("."))
Beispiel #22
0
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)
Beispiel #23
0
 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("!")
Beispiel #24
0
    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
Beispiel #26
0
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
Beispiel #27
0
    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
Beispiel #28
0
    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"), [])
Beispiel #29
0
    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})
Beispiel #30
0
    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") ])
Beispiel #31
0
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)
Beispiel #34
0
 def to_string(self):
     hash = u("%s%s") % (self.salt, self.checksum)
     return uascii_to_str(hash)
Beispiel #35
0
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")
Beispiel #37
0
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:
Beispiel #38
0
# 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
# =============================================================================
Beispiel #39
0
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()
Beispiel #40
0
 def to_string(self):
     chk = self.checksum
     hash = u("S:%s%s") % (chk.upper(), self.salt.upper())
     return uascii_to_str(hash)
Beispiel #41
0
 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!")
Beispiel #43
0
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,
        )
Beispiel #44
0
 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********************'),
            ))
Beispiel #46
0
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")}
Beispiel #48
0
    "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()`.
    """
Beispiel #49
0
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")
Beispiel #50
0
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")
Beispiel #51
0
 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')
Beispiel #54
0
    "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)
Beispiel #56
0
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:
Beispiel #58
0
    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,
        )
Beispiel #59
0
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)))
Beispiel #60
0
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")