class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper):
    """test django_bcrypt_sha256"""

    handler = hash.django_bcrypt_sha256
    forbidden_characters = None
    fuzz_salts_need_bcrypt_repair = True

    known_correct_hashes = [
        #
        # custom - generated via django 1.6 hasher
        #
        (
            "",
            "bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu",
        ),
        (
            UPASS_LETMEIN,
            "bcrypt_sha256$$2a$08$NDjSAIcas.EcoxCRiArvT.MkNiPYVhrsrnJsRkLueZOoV1bsQqlmC",
        ),
        (
            UPASS_TABLE,
            "bcrypt_sha256$$2a$06$kCXUnRFQptGg491siDKNTu8RxjBGSjALHRuvhPYNFsa4Ea5d9M48u",
        ),
        # test >72 chars is hashed correctly -- under bcrypt these hash the same.
        (
            repeat_string("abc123", 72),
            "bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OySmyXA8FoY4PjGizjE1QSDfuL5MXNni",
        ),
        (
            repeat_string("abc123", 72) + "qwr",
            "bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61Ocy0BEz1RK6xslSNi8PlaLX2pe7x/KQG",
        ),
        (
            repeat_string("abc123", 72) + "xyz",
            "bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OvY2zoRVUa2Pugv2ExVOUT2YmhvxUFUa",
        ),
    ]

    known_malformed_hashers = [
        # data in django salt field
        "bcrypt_sha256$xyz$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu"
    ]

    # NOTE: the following have been cloned from _bcrypt_test()

    def populate_settings(self, kwds):
        # speed up test w/ lower rounds
        kwds.setdefault("rounds", 4)
        super(django_bcrypt_sha256_test, self).populate_settings(kwds)

    class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
        def random_rounds(self):
            # decrease default rounds for fuzz testing to speed up volume.
            return self.randintgauss(5, 8, 6, 1)

        def random_ident(self):
            # omit multi-ident tests, only $2a$ counts for this class
            # XXX: enable this to check 2a / 2b?
            return None
예제 #2
0
class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper):
    "test django_bcrypt_sha256"
    handler = hash.django_bcrypt_sha256
    min_django_version = (1,6)
    forbidden_characters = None
    fuzz_salts_need_bcrypt_repair = True

    known_correct_hashes = [
        #
        # custom - generated via django 1.6 hasher
        #
        ('',
            'bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu'),
        (UPASS_LETMEIN,
            'bcrypt_sha256$$2a$08$NDjSAIcas.EcoxCRiArvT.MkNiPYVhrsrnJsRkLueZOoV1bsQqlmC'),
        (UPASS_TABLE,
            'bcrypt_sha256$$2a$06$kCXUnRFQptGg491siDKNTu8RxjBGSjALHRuvhPYNFsa4Ea5d9M48u'),

        # test >72 chars is hashed correctly -- under bcrypt these hash the same.
        (repeat_string("abc123",72),
            'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OySmyXA8FoY4PjGizjE1QSDfuL5MXNni'),
        (repeat_string("abc123",72)+"qwr",
            'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61Ocy0BEz1RK6xslSNi8PlaLX2pe7x/KQG'),
        (repeat_string("abc123",72)+"xyz",
            'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OvY2zoRVUa2Pugv2ExVOUT2YmhvxUFUa'),
    ]

    known_malformed_hashers = [
        # data in django salt field
        'bcrypt_sha256$xyz$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu',
    ]

    def test_30_HasManyIdents(self):
        raise self.skipTest("multiple idents not supported")

    def test_30_HasOneIdent(self):
        # forbidding ident keyword, django doesn't support configuring this
        handler = self.handler
        handler(use_defaults=True)
        self.assertRaises(TypeError, handler, ident="$2a$", use_defaults=True)

    # NOTE: the following have been cloned from _bcrypt_test()

    def populate_settings(self, kwds):
        # speed up test w/ lower rounds
        kwds.setdefault("rounds", 4)
        super(django_bcrypt_sha256_test, self).populate_settings(kwds)

    def fuzz_setting_rounds(self):
        # decrease default rounds for fuzz testing to speed up volume.
        return randintgauss(5, 8, 6, 1)

    def fuzz_setting_ident(self):
        # omit multi-ident tests, only $2a$ counts for this class
        return None
예제 #3
0
    def _calc_checksum(self, secret):
        """common backend code"""

        # make sure it's unicode
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")

        # NOTE: especially important to forbid NULLs for bcrypt, since many
        # backends (bcryptor, bcrypt) happily accept them, and then
        # silently truncate the password at first NULL they encounter!
        if _BNULL in secret:
            raise uh.exc.NullPasswordError(self)

        # ensure backend is loaded before workaround detection
        self.get_backend()

        # protect from wraparound bug by truncating secret before handing it to the backend.
        # bcrypt only uses first 72 bytes anyways.
        if self._has_wraparound_bug and len(secret) >= 255:
            secret = secret[:72]

        # special case handling for variants (ordered most common first)
        ident = self.ident
        if ident == IDENT_2A:
            # fall through and use backend w/o hacks
            pass

        elif ident == IDENT_2B:
            if self._lacks_2b_support:
                # handle $2b$ hash format even if backend is too old.
                # have it generate a 2A digest, then return it as a 2B hash.
                ident = IDENT_2A

        elif ident == IDENT_2Y:
            if self._lacks_2y_support:
                # handle $2y$ hash format (not supported by BSDs, being phased out on others)
                # have it generate a 2A digest, then return it as a 2Y hash.
                ident = IDENT_2A

        elif ident == IDENT_2:
            if self._lacks_20_support:
                # handle legacy $2$ format (not supported by most backends except BSD os_crypt)
                # 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)
                ident = IDENT_2A

        elif ident == IDENT_2X:

            # NOTE: shouldn't get here.
            # XXX: could check if backend does actually offer 'support'
            raise RuntimeError("$2x$ hashes not currently supported by passlib")

        else:
            raise AssertionError("unexpected ident value: %r" % ident)

        # invoke backend
        config = self._get_config(ident)
        return self._calc_checksum_backend(secret, config)
예제 #4
0
 def check_bcryptor(secret, hash):
     "bcryptor"
     secret = to_native_str(secret, self.fuzz_password_encoding)
     if hash.startswith(IDENT_2Y):
         hash = IDENT_2A + hash[4:]
     elif hash.startswith(IDENT_2):
         # bcryptor doesn't support $2$ hashes; but we can fake it
         # using the $2a$ algorithm, by repeating the password until
         # it's 72 chars in length.
         hash = IDENT_2A + hash[3:]
         if secret:
             secret = repeat_string(secret, 72)
     return Engine(False).hash_key(secret, hash) == hash
예제 #5
0
 def get_fuzz_settings(self):
     secret, other, kwds = super(_bcrypt_test,self).get_fuzz_settings()
     from passlib.handlers.bcrypt import IDENT_2, IDENT_2X
     from passlib.utils import to_bytes
     ident = kwds.get('ident')
     if ident == IDENT_2X:
         # 2x is just recognized, not supported. don't test with it.
         del kwds['ident']
     elif ident == IDENT_2 and other and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret):
         # avoid false failure due to flaw in 0-revision bcrypt:
         # repeated strings like 'abc' and 'abcabc' hash identically.
         other = self.get_fuzz_password()
     return secret, other, kwds
예제 #6
0
 def check_bcryptor(secret, hash):
     """bcryptor"""
     secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding)
     if hash.startswith((IDENT_2B, IDENT_2Y)):
         hash = IDENT_2A + hash[4:]
     elif hash.startswith(IDENT_2):
         # bcryptor doesn't support $2$ hashes; but we can fake it
         # using the $2a$ algorithm, by repeating the password until
         # it's 72 chars in length.
         hash = IDENT_2A + hash[3:]
         if secret:
             secret = repeat_string(secret, 72)
     return Engine(False).hash_key(secret, hash) == hash
예제 #7
0
 def get_fuzz_settings(self):
     secret, other, kwds = super(_bcrypt_test, self).get_fuzz_settings()
     from passlib.handlers.bcrypt import IDENT_2, IDENT_2X
     from passlib.utils import to_bytes
     ident = kwds.get('ident')
     if ident == IDENT_2X:
         # 2x is just recognized, not supported. don't test with it.
         del kwds['ident']
     elif ident == IDENT_2 and other and repeat_string(
             to_bytes(other), len(to_bytes(secret))) == to_bytes(secret):
         # avoid false failure due to flaw in 0-revision bcrypt:
         # repeated strings like 'abc' and 'abcabc' hash identically.
         other = self.get_fuzz_password()
     return secret, other, kwds
예제 #8
0
    def key_to_words(data, size=18):
        """convert data to tuple of <size> 4-byte integers, repeating or
        truncating data as needed to reach specified size"""
        assert isinstance(data, bytes)
        dlen = len(data)
        if not dlen:
            # return all zeros - original C code would just read the NUL after
            # the password, so mimicing that behavior for this edge case.
            return [0] * size

        # repeat data until it fills up 4*size bytes
        data = repeat_string(data, size << 2)

        # unpack
        return struct.unpack(">%dI" % (size, ), data)
예제 #9
0
파일: base.py 프로젝트: dragoncsc/HDsite
    def key_to_words(data, size=18):
        """convert data to tuple of <size> 4-byte integers, repeating or
        truncating data as needed to reach specified size"""
        assert isinstance(data, bytes)
        dlen = len(data)
        if not dlen:
            # return all zeros - original C code would just read the NUL after
            # the password, so mimicing that behavior for this edge case.
            return [0]*size

        # repeat data until it fills up 4*size bytes
        data = repeat_string(data, size<<2)

        # unpack
        return struct.unpack(">%dI" % (size,), data)
예제 #10
0
 def check_bcrypt(secret, hash):
     """bcrypt"""
     secret = to_bytes(secret, self.fuzz_password_encoding)
     # if hash.startswith(IDENT_2Y):
     #    hash = IDENT_2A + hash[4:]
     if hash.startswith(IDENT_2):
         # bcryptor doesn't support $2$ hashes; but we can fake it
         # using the $2a$ algorithm, by repeating the password until
         # it's 72 chars in length.
         hash = IDENT_2A + hash[3:]
         if secret:
             secret = repeat_string(secret, 72)
     hash = to_bytes(hash)
     try:
         return bcrypt.hashpw(secret, hash) == hash
     except ValueError:
         raise ValueError("bcrypt rejected hash: %r" % (hash,))
예제 #11
0
파일: bcrypt.py 프로젝트: cutso/passlib
 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 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:])
예제 #12
0
파일: bcrypt.py 프로젝트: thebaron/passlib
 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 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:])
예제 #13
0
 def check_bcrypt(secret, hash):
     "bcrypt"
     secret = to_bytes(secret, self.fuzz_password_encoding)
     #if hash.startswith(IDENT_2Y):
     #    hash = IDENT_2A + hash[4:]
     if hash.startswith(IDENT_2):
         # bcryptor doesn't support $2$ hashes; but we can fake it
         # using the $2a$ algorithm, by repeating the password until
         # it's 72 chars in length.
         hash = IDENT_2A + hash[3:]
         if secret:
             secret = repeat_string(secret, 72)
     hash = to_bytes(hash)
     try:
         return bcrypt.hashpw(secret, hash) == hash
     except ValueError:
         raise ValueError("bcrypt rejected hash: %r" % (hash, ))
예제 #14
0
        def generate(self):
            opts = super(_bcrypt_test.FuzzHashGenerator, self).generate()

            secret = opts['secret']
            other = opts['other']
            settings = opts['settings']
            ident = settings.get('ident')

            if ident == IDENT_2X:
                # 2x is just recognized, not supported. don't test with it.
                del settings['ident']

            elif ident == IDENT_2 and other and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret):
                # avoid false failure due to flaw in 0-revision bcrypt:
                # repeated strings like 'abc' and 'abcabc' hash identically.
                opts['secret'], opts['other'] = self.random_password_pair()

            return opts
예제 #15
0
파일: bcrypt.py 프로젝트: cutso/passlib
 def _calc_checksum_bcrypt(self, secret):
     # bcrypt behavior:
     #   hash must be ascii bytes
     #   secret must be bytes
     #   returns bytes
     if self.ident == IDENT_2:
         # bcrypt 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()
     if isinstance(config, unicode):
         config = config.encode("ascii")
     hash = _bcrypt.hashpw(secret, config)
     assert hash.startswith(config) and len(hash) == len(config)+31
     assert isinstance(hash, bytes)
     return hash[-31:].decode("ascii")
예제 #16
0
파일: bcrypt.py 프로젝트: thebaron/passlib
 def _calc_checksum_bcrypt(self, secret):
     # bcrypt behavior:
     #   hash must be ascii bytes
     #   secret must be bytes
     #   returns bytes
     if self.ident == IDENT_2:
         # bcrypt 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()
     if isinstance(config, unicode):
         config = config.encode("ascii")
     hash = _bcrypt.hashpw(secret, config)
     assert hash.startswith(config) and len(hash) == len(config) + 31
     assert isinstance(hash, bytes)
     return hash[-31:].decode("ascii")
예제 #17
0
        def generate(self):
            opts = super(_bcrypt_test.FuzzHashGenerator, self).generate()

            secret = opts['secret']
            other = opts['other']
            settings = opts['settings']
            ident = settings.get('ident')

            if ident == IDENT_2X:
                # 2x is just recognized, not supported. don't test with it.
                del settings['ident']

            elif ident == IDENT_2 and other and repeat_string(
                    to_bytes(other), len(
                        to_bytes(secret))) == to_bytes(secret):
                # avoid false failure due to flaw in 0-revision bcrypt:
                # repeated strings like 'abc' and 'abcabc' hash identically.
                opts['secret'], opts['other'] = self.random_password_pair()

            return opts
예제 #18
0
 def check_bcrypt(secret, hash):
     """bcrypt"""
     secret = to_bytes(secret, self.FuzzHashGenerator.password_encoding)
     if hash.startswith(IDENT_2B):
         # bcrypt <1.1 lacks 2B support
         hash = IDENT_2A + hash[4:]
     elif hash.startswith(IDENT_2):
         # bcrypt doesn't support $2$ hashes; but we can fake it
         # using the $2a$ algorithm, by repeating the password until
         # it's 72 chars in length.
         hash = IDENT_2A + hash[3:]
         if secret:
             secret = repeat_string(secret, 72)
     elif hash.startswith(IDENT_2Y) and bcrypt.__version__ == "3.0.0":
         hash = IDENT_2B + hash[4:]
     hash = to_bytes(hash)
     try:
         return bcrypt.hashpw(secret, hash) == hash
     except ValueError:
         raise ValueError("bcrypt rejected hash: %r (secret=%r)" % (hash, secret))
예제 #19
0
 def check_bcrypt(secret, hash):
     """bcrypt"""
     secret = to_bytes(secret, self.FuzzHashGenerator.password_encoding)
     if hash.startswith(IDENT_2B):
         # bcrypt <1.1 lacks 2B support
         hash = IDENT_2A + hash[4:]
     elif hash.startswith(IDENT_2):
         # bcrypt doesn't support $2$ hashes; but we can fake it
         # using the $2a$ algorithm, by repeating the password until
         # it's 72 chars in length.
         hash = IDENT_2A + hash[3:]
         if secret:
             secret = repeat_string(secret, 72)
     elif hash.startswith(IDENT_2Y) and bcrypt.__version__ == "3.0.0":
         hash = IDENT_2B + hash[4:]
     hash = to_bytes(hash)
     try:
         return bcrypt.hashpw(secret, hash) == hash
     except ValueError:
         raise ValueError("bcrypt rejected hash: %r (secret=%r)" %
                          (hash, secret))
예제 #20
0
 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:])
예제 #21
0
 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:])
예제 #22
0
def _raw_sha2_crypt(pwd, salt, rounds, use_512=False):
    """perform raw sha256-crypt / sha512-crypt

    this function provides a pure-python implementation of the internals
    for the SHA256-Crypt and SHA512-Crypt algorithms; it doesn't
    handle any of the parsing/validation of the hash strings themselves.

    :arg pwd: password chars/bytes to hash
    :arg salt: salt chars to use
    :arg rounds: linear rounds cost
    :arg use_512: use sha512-crypt instead of sha256-crypt mode

    :returns:
        encoded checksum chars
    """
    # ===================================================================
    # init & validate inputs
    # ===================================================================

    # NOTE: the setup portion of this algorithm scales ~linearly in time
    #       with the size of the password, making it vulnerable to a DOS from
    #       unreasonably large inputs. the following code has some optimizations
    #       which would make things even worse, using O(pwd_len**2) memory
    #       when calculating digest P.
    #
    #       to mitigate these two issues: 1) this code switches to a
    #       O(pwd_len)-memory algorithm for passwords that are much larger
    #       than average, and 2) Passlib enforces a library-wide max limit on
    #       the size of passwords it will allow, to prevent this algorithm and
    #       others from being DOSed in this way (see passlib.exc.PasswordSizeError
    #       for details).

    # validate secret
    if isinstance(pwd, unicode):
        # XXX: not sure what official unicode policy is, using this as default
        pwd = pwd.encode("utf-8")
    assert isinstance(pwd, bytes)
    if _BNULL in pwd:
        raise uh.exc.NullPasswordError(
            sha512_crypt if use_512 else sha256_crypt)
    pwd_len = len(pwd)

    # validate rounds
    assert 1000 <= rounds <= 999999999, "invalid rounds"
    # NOTE: spec says out-of-range rounds should be clipped, instead of
    # causing an error. this function assumes that's been taken care of
    # by the handler class.

    # validate salt
    assert isinstance(salt, unicode), "salt not unicode"
    salt = salt.encode("ascii")
    salt_len = len(salt)
    assert salt_len < 17, "salt too large"
    # NOTE: spec says salts larger than 16 bytes should be truncated,
    # instead of causing an error. this function assumes that's been
    # taken care of by the handler class.

    # load sha256/512 specific constants
    if use_512:
        hash_const = hashlib.sha512
        transpose_map = _512_transpose_map
    else:
        hash_const = hashlib.sha256
        transpose_map = _256_transpose_map

    # ===================================================================
    # digest B - used as subinput to digest A
    # ===================================================================
    db = hash_const(pwd + salt + pwd).digest()

    # ===================================================================
    # digest A - used to initialize first round of digest C
    # ===================================================================
    # start out with pwd + salt
    a_ctx = hash_const(pwd + salt)
    a_ctx_update = a_ctx.update

    # add pwd_len bytes of b, repeating b as many times as needed.
    a_ctx_update(repeat_string(db, pwd_len))

    # for each bit in pwd_len: add b if it's 1, or pwd if it's 0
    i = pwd_len
    while i:
        a_ctx_update(db if i & 1 else pwd)
        i >>= 1

    # finish A
    da = a_ctx.digest()

    # ===================================================================
    # digest P from password - used instead of password itself
    #                          when calculating digest C.
    # ===================================================================
    if pwd_len < 96:
        # this method is faster under python, but uses O(pwd_len**2) memory;
        # so we don't use it for larger passwords to avoid a potential DOS.
        dp = repeat_string(hash_const(pwd * pwd_len).digest(), pwd_len)
    else:
        # this method is slower under python, but uses a fixed amount of memory.
        tmp_ctx = hash_const(pwd)
        tmp_ctx_update = tmp_ctx.update
        i = pwd_len - 1
        while i:
            tmp_ctx_update(pwd)
            i -= 1
        dp = repeat_string(tmp_ctx.digest(), pwd_len)
    assert len(dp) == pwd_len

    # ===================================================================
    # digest S  - used instead of salt itself when calculating digest C
    # ===================================================================
    ds = hash_const(salt * (16 + byte_elem_value(da[0]))).digest()[:salt_len]
    assert len(ds) == salt_len, "salt_len somehow > hash_len!"

    # ===================================================================
    # digest C - for a variable number of rounds, combine A, S, and P
    #            digests in various ways; in order to burn CPU time.
    # ===================================================================

    # NOTE: the original SHA256/512-Crypt specification performs the C digest
    # calculation using the following loop:
    #
    ##dc = da
    ##i = 0
    # while i < rounds:
    ##    tmp_ctx = hash_const(dp if i & 1 else dc)
    # if i % 3:
    # tmp_ctx.update(ds)
    # if i % 7:
    # tmp_ctx.update(dp)
    ##    tmp_ctx.update(dc if i & 1 else dp)
    ##    dc = tmp_ctx.digest()
    ##    i += 1
    #
    # The code Passlib uses (below) implements an equivalent algorithm,
    # it's just been heavily optimized to pre-calculate a large number
    # of things beforehand. It works off of a couple of observations
    # about the original algorithm:
    #
    # 1. each round is a combination of 'dc', 'ds', and 'dp'; determined
    #    by the whether 'i' a multiple of 2,3, and/or 7.
    # 2. since lcm(2,3,7)==42, the series of combinations will repeat
    #    every 42 rounds.
    # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
    #    while odd rounds 1-41 consist of hash(round-specific-constant + dc)
    #
    # Using these observations, the following code...
    # * calculates the round-specific combination of ds & dp for each round 0-41
    # * runs through as many 42-round blocks as possible
    # * runs through as many pairs of rounds as possible for remaining rounds
    # * performs once last round if the total rounds should be odd.
    #
    # this cuts out a lot of the control overhead incurred when running the
    # original loop 40,000+ times in python, resulting in ~20% increase in
    # speed under CPython (though still 2x slower than glibc crypt)

    # prepare the 6 combinations of ds & dp which are needed
    # (order of 'perms' must match how _c_digest_offsets was generated)
    dp_dp = dp + dp
    dp_ds = dp + ds
    perms = [dp, dp_dp, dp_ds, dp_ds + dp, ds + dp, ds + dp_dp]

    # build up list of even-round & odd-round constants,
    # and store in 21-element list as (even,odd) pairs.
    data = [(perms[even], perms[odd]) for even, odd in _c_digest_offsets]

    # perform as many full 42-round blocks as possible
    dc = da
    blocks, tail = divmod(rounds, 42)
    while blocks:
        for even, odd in data:
            dc = hash_const(odd + hash_const(dc + even).digest()).digest()
        blocks -= 1

    # perform any leftover rounds
    if tail:
        # perform any pairs of rounds
        pairs = tail >> 1
        for even, odd in data[:pairs]:
            dc = hash_const(odd + hash_const(dc + even).digest()).digest()

        # if rounds was odd, do one last round (since we started at 0,
        # last round will be an even-numbered round)
        if tail & 1:
            dc = hash_const(dc + data[pairs][0]).digest()

    # ===================================================================
    # encode digest using appropriate transpose map
    # ===================================================================
    return h64.encode_transposed_bytes(dc, transpose_map).decode("ascii")
예제 #23
0
    def _calc_checksum(self, secret):
        """
        This function implements the "encrypted" hash format used by Cisco
        PIX & ASA. It's behavior has been confirmed for ASA 9.6,
        but is presumed correct for PIX & other ASA releases,
        as it fits with known test vectors, and existing literature.

        While nearly the same, the PIX & ASA hashes have slight differences,
        so this function performs differently based on the _is_asa class flag.
        Noteable changes from PIX to ASA include password size limit
        increased from 16 -> 32, and other internal changes.
        """
        # select PIX vs or ASA mode
        asa = self._is_asa

        #
        # encode secret
        #
        # per ASA 8.4 documentation,
        # http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets,
        # it supposedly uses UTF-8 -- though some double-encoding issues have
        # been observed when trying to actually *set* a non-ascii password
        # via ASDM, and access via SSH seems to strip 8-bit chars.
        #
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")

        #
        # check if password too large
        #
        # Per ASA 9.6 changes listed in
        # http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html,
        # prior releases had a maximum limit of 32 characters.
        # Testing with an ASA 9.6 system bears this out --
        # setting 32-char password for a user account,
        # and logins will fail if any chars are appended.
        # (ASA 9.6 added new PBKDF2-based hash algorithm,
        #  which supports larger passwords).
        #
        # Per PIX documentation
        # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html,
        # it would not allow passwords > 16 chars.
        #
        # Thus, we unconditionally throw a password size error here,
        # as nothing valid can come from a larger password.
        # NOTE: assuming PIX has same behavior, but at 16 char limit.
        #
        spoil_digest = None
        if len(secret) > self.truncate_size:
            if self.use_defaults:
                # called from hash()
                msg = "Password too long (%s allows at most %d bytes)" % (
                    self.name,
                    self.truncate_size,
                )
                raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg)
            else:
                # called from verify() --
                # We don't want to throw error, or return early,
                # as that would let attacker know too much.  Instead, we set a
                # flag to add some dummy data into the md5 digest, so that
                # output won't match truncated version of secret, or anything
                # else that's fixed and predictable.
                spoil_digest = secret + _DUMMY_BYTES

        #
        # append user to secret
        #
        # Policy appears to be:
        #
        # * Nothing appended for enable password (user = "")
        #
        # * ASA: If user present, but secret is >= 28 chars, nothing appended.
        #
        # * 1-2 byte users not allowed.
        #   DEVIATION: we're letting them through, and repeating their
        #   chars ala 3-char user, to simplify testing.
        #   Could issue warning in the future though.
        #
        # * 3 byte user has first char repeated, to pad to 4.
        #   (observed under ASA 9.6, assuming true elsewhere)
        #
        # * 4 byte users are used directly.
        #
        # * 5+ byte users are truncated to 4 bytes.
        #
        user = self.user
        if user:
            if isinstance(user, unicode):
                user = user.encode("utf-8")
            if not asa or len(secret) < 28:
                secret += repeat_string(user, 4)

        #
        # pad / truncate result to limit
        #
        # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF
        # secret+user > 16 bytes.  This makes PIX & ASA have different results
        # where secret size in range(13,16), and user is present --
        # PIX will truncate to 16, ASA will truncate to 32.
        #
        if asa and len(secret) > 16:
            pad_size = 32
        else:
            pad_size = 16
        secret = right_pad_string(secret, pad_size)

        #
        # md5 digest
        #
        if spoil_digest:
            # make sure digest won't match truncated version of secret
            secret += spoil_digest
        digest = md5(secret).digest()

        #
        # drop every 4th byte
        # NOTE: guessing this was done because it makes output exactly
        #       16 bytes, which may have been a general 'char password[]'
        #       size limit under PIX
        #
        digest = join_byte_elems(c for i, c in enumerate(digest)
                                 if (i + 1) & 3)

        #
        # encode using Hash64
        #
        return h64.encode_bytes(digest).decode("ascii")
예제 #24
0
    def _norm_digest_args(cls, secret, ident, new=False):
        # make sure secret is unicode
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")

        # check max secret size
        uh.validate_secret(secret)

        # check for truncation (during .hash() calls only)
        if new:
            cls._check_truncate_policy(secret)

        # NOTE: especially important to forbid NULLs for bcrypt, since many
        # backends (bcryptor, bcrypt) happily accept them, and then
        # silently truncate the password at first NULL they encounter!
        if _BNULL in secret:
            raise uh.exc.NullPasswordError(cls)

        # TODO: figure out way to skip these tests when not needed...

        # protect from wraparound bug by truncating secret before handing it to the backend.
        # bcrypt only uses first 72 bytes anyways.
        # NOTE: not needed for 2y/2b, but might use 2a as fallback for them.
        if cls._has_2a_wraparound_bug and len(secret) >= 255:
            secret = secret[:72]

        # special case handling for variants (ordered most common first)
        if ident == IDENT_2A:
            # nothing needs to be done.
            pass

        elif ident == IDENT_2B:
            if cls._lacks_2b_support:
                # handle $2b$ hash format even if backend is too old.
                # have it generate a 2A/2Y digest, then return it as a 2B hash.
                # 2a-only backend could potentially exhibit wraparound bug --
                # but we work around that issue above.
                ident = cls._fallback_ident

        elif ident == IDENT_2Y:
            if cls._lacks_2y_support:
                # handle $2y$ hash format (not supported by BSDs, being phased out on others)
                # have it generate a 2A/2B digest, then return it as a 2Y hash.
                ident = cls._fallback_ident

        elif ident == IDENT_2:
            if cls._lacks_20_support:
                # handle legacy $2$ format (not supported by most backends except BSD os_crypt)
                # we can fake $2$ behavior using the 2A/2Y/2B algorithm
                # by repeating the password until it's at least 72 chars in length.
                if secret:
                    secret = repeat_string(secret, 72)
                ident = cls._fallback_ident

        elif ident == IDENT_2X:

            # NOTE: shouldn't get here.
            # XXX: could check if backend does actually offer 'support'
            raise RuntimeError(
                "$2x$ hashes not currently supported by passlib")

        else:
            raise AssertionError("unexpected ident value: %r" % ident)

        return secret, ident
예제 #25
0
def _raw_md5_crypt(pwd, salt, use_apr=False):
    """perform raw md5-crypt calculation

    this function provides a pure-python implementation of the internals
    for the MD5-Crypt algorithms; it doesn't handle any of the
    parsing/validation of the hash strings themselves.

    :arg pwd: password chars/bytes to encrypt
    :arg salt: salt chars to use
    :arg use_apr: use apache variant

    :returns:
        encoded checksum chars
    """
    # NOTE: regarding 'apr' format:
    # really, apache? you had to invent a whole new "$apr1$" format,
    # when all you did was change the ident incorporated into the hash?
    # would love to find webpage explaining why just using a portable
    # implementation of $1$ wasn't sufficient. *nothing else* was changed.

    #===================================================================
    # init & validate inputs
    #===================================================================

    # validate secret
    # XXX: not sure what official unicode policy is, using this as default
    if isinstance(pwd, unicode):
        pwd = pwd.encode("utf-8")
    assert isinstance(pwd, bytes), "pwd not unicode or bytes"
    if _BNULL in pwd:
        raise uh.exc.NullPasswordError(md5_crypt)
    pwd_len = len(pwd)

    # validate salt - should have been taken care of by caller
    assert isinstance(salt, unicode), "salt not unicode"
    salt = salt.encode("ascii")
    assert len(salt) < 9, "salt too large"
    # NOTE: spec says salts larger than 8 bytes should be truncated,
    # instead of causing an error. this function assumes that's been
    # taken care of by the handler class.

    # load APR specific constants
    if use_apr:
        magic = _APR_MAGIC
    else:
        magic = _MD5_MAGIC

    #===================================================================
    # digest B - used as subinput to digest A
    #===================================================================
    db = md5(pwd + salt + pwd).digest()

    #===================================================================
    # digest A - used to initialize first round of digest C
    #===================================================================
    # start out with pwd + magic + salt
    a_ctx = md5(pwd + magic + salt)
    a_ctx_update = a_ctx.update

    # add pwd_len bytes of b, repeating b as many times as needed.
    a_ctx_update(repeat_string(db, pwd_len))

    # add null chars & first char of password
    # NOTE: this may have historically been a bug,
    # where they meant to use db[0] instead of B_NULL,
    # but the original code memclear'ed db,
    # and now all implementations have to use this.
    i = pwd_len
    evenchar = pwd[:1]
    while i:
        a_ctx_update(_BNULL if i & 1 else evenchar)
        i >>= 1

    # finish A
    da = a_ctx.digest()

    #===================================================================
    # digest C - for a 1000 rounds, combine A, S, and P
    #            digests in various ways; in order to burn CPU time.
    #===================================================================

    # NOTE: the original MD5-Crypt implementation performs the C digest
    # calculation using the following loop:
    #
    ##dc = da
    ##i = 0
    ##while i < rounds:
    ##    tmp_ctx = md5(pwd if i & 1 else dc)
    ##    if i % 3:
    ##        tmp_ctx.update(salt)
    ##    if i % 7:
    ##        tmp_ctx.update(pwd)
    ##    tmp_ctx.update(dc if i & 1 else pwd)
    ##    dc = tmp_ctx.digest()
    ##    i += 1
    #
    # The code Passlib uses (below) implements an equivalent algorithm,
    # it's just been heavily optimized to pre-calculate a large number
    # of things beforehand. It works off of a couple of observations
    # about the original algorithm:
    #
    # 1. each round is a combination of 'dc', 'salt', and 'pwd'; and the exact
    #    combination is determined by whether 'i' a multiple of 2,3, and/or 7.
    # 2. since lcm(2,3,7)==42, the series of combinations will repeat
    #    every 42 rounds.
    # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
    #    while odd rounds 1-41 consist of hash(round-specific-constant + dc)
    #
    # Using these observations, the following code...
    # * calculates the round-specific combination of salt & pwd for each round 0-41
    # * runs through as many 42-round blocks as possible (23)
    # * runs through as many pairs of rounds as needed for remaining rounds (17)
    # * this results in the required 42*23+2*17=1000 rounds required by md5_crypt.
    #
    # this cuts out a lot of the control overhead incurred when running the
    # original loop 1000 times in python, resulting in ~20% increase in
    # speed under CPython (though still 2x slower than glibc crypt)

    # prepare the 6 combinations of pwd & salt which are needed
    # (order of 'perms' must match how _c_digest_offsets was generated)
    pwd_pwd = pwd + pwd
    pwd_salt = pwd + salt
    perms = [
        pwd, pwd_pwd, pwd_salt, pwd_salt + pwd, salt + pwd, salt + pwd_pwd
    ]

    # build up list of even-round & odd-round constants,
    # and store in 21-element list as (even,odd) pairs.
    data = [(perms[even], perms[odd]) for even, odd in _c_digest_offsets]

    # perform 23 blocks of 42 rounds each (for a total of 966 rounds)
    dc = da
    blocks = 23
    while blocks:
        for even, odd in data:
            dc = md5(odd + md5(dc + even).digest()).digest()
        blocks -= 1

    # perform 17 more pairs of rounds (34 more rounds, for a total of 1000)
    for even, odd in data[:17]:
        dc = md5(odd + md5(dc + even).digest()).digest()

    #===================================================================
    # encode digest using appropriate transpose map
    #===================================================================
    return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii")
예제 #26
0
파일: cisco.py 프로젝트: javierrami/GAM
    def _calc_checksum(self, secret):
        """
        This function implements the "encrypted" hash format used by Cisco
        PIX & ASA. It's behavior has been confirmed for ASA 9.6,
        but is presumed correct for PIX & other ASA releases,
        as it fits with known test vectors, and existing literature.

        While nearly the same, the PIX & ASA hashes have slight differences,
        so this function performs differently based on the _is_asa class flag.
        Noteable changes from PIX to ASA include password size limit
        increased from 16 -> 32, and other internal changes.
        """
        # select PIX vs or ASA mode
        asa = self._is_asa

        #
        # encode secret
        #
        # per ASA 8.4 documentation,
        # http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets,
        # it supposedly uses UTF-8 -- though some double-encoding issues have
        # been observed when trying to actually *set* a non-ascii password
        # via ASDM, and access via SSH seems to strip 8-bit chars.
        #
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")

        #
        # check if password too large
        #
        # Per ASA 9.6 changes listed in
        # http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html,
        # prior releases had a maximum limit of 32 characters.
        # Testing with an ASA 9.6 system bears this out --
        # setting 32-char password for a user account,
        # and logins will fail if any chars are appended.
        # (ASA 9.6 added new PBKDF2-based hash algorithm,
        #  which supports larger passwords).
        #
        # Per PIX documentation
        # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html,
        # it would not allow passwords > 16 chars.
        #
        # Thus, we unconditionally throw a password size error here,
        # as nothing valid can come from a larger password.
        # NOTE: assuming PIX has same behavior, but at 16 char limit.
        #
        spoil_digest = None
        if len(secret) > self.truncate_size:
            if self.use_defaults:
                # called from hash()
                msg = "Password too long (%s allows at most %d bytes)" % \
                      (self.name, self.truncate_size)
                raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg)
            else:
                # called from verify() --
                # We don't want to throw error, or return early,
                # as that would let attacker know too much.  Instead, we set a
                # flag to add some dummy data into the md5 digest, so that
                # output won't match truncated version of secret, or anything
                # else that's fixed and predictable.
                spoil_digest = secret + _DUMMY_BYTES

        #
        # append user to secret
        #
        # Policy appears to be:
        #
        # * Nothing appended for enable password (user = "")
        #
        # * ASA: If user present, but secret is >= 28 chars, nothing appended.
        #
        # * 1-2 byte users not allowed.
        #   DEVIATION: we're letting them through, and repeating their
        #   chars ala 3-char user, to simplify testing.
        #   Could issue warning in the future though.
        #
        # * 3 byte user has first char repeated, to pad to 4.
        #   (observed under ASA 9.6, assuming true elsewhere)
        #
        # * 4 byte users are used directly.
        #
        # * 5+ byte users are truncated to 4 bytes.
        #
        user = self.user
        if user:
            if isinstance(user, unicode):
                user = user.encode("utf-8")
            if not asa or len(secret) < 28:
                secret += repeat_string(user, 4)

        #
        # pad / truncate result to limit
        #
        # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF
        # secret+user > 16 bytes.  This makes PIX & ASA have different results
        # where secret size in range(13,16), and user is present --
        # PIX will truncate to 16, ASA will truncate to 32.
        #
        if asa and len(secret) > 16:
            pad_size = 32
        else:
            pad_size = 16
        secret = right_pad_string(secret, pad_size)

        #
        # md5 digest
        #
        if spoil_digest:
            # make sure digest won't match truncated version of secret
            secret += spoil_digest
        digest = md5(secret).digest()

        #
        # drop every 4th byte
        # NOTE: guessing this was done because it makes output exactly
        #       16 bytes, which may have been a general 'char password[]'
        #       size limit under PIX
        #
        digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3)

        #
        # encode using Hash64
        #
        return h64.encode_bytes(digest).decode("ascii")
예제 #27
0
파일: sha2_crypt.py 프로젝트: cutso/passlib
def _raw_sha2_crypt(pwd, salt, rounds, use_512=False):
    """perform raw sha256-crypt / sha512-crypt

    this function provides a pure-python implementation of the internals
    for the SHA256-Crypt and SHA512-Crypt algorithms; it doesn't
    handle any of the parsing/validation of the hash strings themselves.

    :arg pwd: password chars/bytes to encrypt
    :arg salt: salt chars to use
    :arg rounds: linear rounds cost
    :arg use_512: use sha512-crypt instead of sha256-crypt mode

    :returns:
        encoded checksum chars
    """
    #===================================================================
    # init & validate inputs
    #===================================================================

    # NOTE: the setup portion of this algorithm scales ~linearly in time
    #       with the size of the password, making it vulnerable to a DOS from
    #       unreasonably large inputs. the following code has some optimizations
    #       which would make things even worse, using O(pwd_len**2) memory
    #       when calculating digest P. 
    #
    #       to mitigate these two issues: 1) this code switches to a 
    #       O(pwd_len)-memory algorithm for passwords that are much larger 
    #       than average, and 2) Passlib enforces a library-wide max limit on
    #       the size of passwords it will allow, to prevent this algorithm and 
    #       others from being DOSed in this way (see passlib.exc.PasswordSizeError
    #       for details).

    # validate secret
    if isinstance(pwd, unicode):
        # XXX: not sure what official unicode policy is, using this as default
        pwd = pwd.encode("utf-8")
    assert isinstance(pwd, bytes)
    if _BNULL in pwd:
        raise uh.exc.NullPasswordError(sha512_crypt if use_512 else sha256_crypt)
    pwd_len = len(pwd)

    # validate rounds
    assert 1000 <= rounds <= 999999999, "invalid rounds"
        # NOTE: spec says out-of-range rounds should be clipped, instead of
        # causing an error. this function assumes that's been taken care of
        # by the handler class.

    # validate salt
    assert isinstance(salt, unicode), "salt not unicode"
    salt = salt.encode("ascii")
    salt_len = len(salt)
    assert salt_len < 17, "salt too large"
        # NOTE: spec says salts larger than 16 bytes should be truncated,
        # instead of causing an error. this function assumes that's been
        # taken care of by the handler class.

    # load sha256/512 specific constants
    if use_512:
        hash_const = hashlib.sha512
        hash_len = 64
        transpose_map = _512_transpose_map
    else:
        hash_const = hashlib.sha256
        hash_len = 32
        transpose_map = _256_transpose_map

    #===================================================================
    # digest B - used as subinput to digest A
    #===================================================================
    db = hash_const(pwd + salt + pwd).digest()

    #===================================================================
    # digest A - used to initialize first round of digest C
    #===================================================================
    # start out with pwd + salt
    a_ctx = hash_const(pwd + salt)
    a_ctx_update = a_ctx.update

    # add pwd_len bytes of b, repeating b as many times as needed.
    a_ctx_update(repeat_string(db, pwd_len))

    # for each bit in pwd_len: add b if it's 1, or pwd if it's 0
    i = pwd_len
    while i:
        a_ctx_update(db if i & 1 else pwd)
        i >>= 1

    # finish A
    da = a_ctx.digest()

    #===================================================================
    # digest P from password - used instead of password itself
    #                          when calculating digest C.
    #===================================================================
    if pwd_len < 96:
        # this method is faster under python, but uses O(pwd_len**2) memory;
        # so we don't use it for larger passwords to avoid a potential DOS.
        dp = repeat_string(hash_const(pwd * pwd_len).digest(), pwd_len)
    else:
        # this method is slower under python, but uses a fixed amount of memory.
        tmp_ctx = hash_const(pwd)
        tmp_ctx_update = tmp_ctx.update
        i = pwd_len-1
        while i:
            tmp_ctx_update(pwd)
            i -= 1
        dp = repeat_string(tmp_ctx.digest(), pwd_len)
    assert len(dp) == pwd_len

    #===================================================================
    # digest S  - used instead of salt itself when calculating digest C
    #===================================================================
    ds = hash_const(salt * (16 + byte_elem_value(da[0]))).digest()[:salt_len]
    assert len(ds) == salt_len, "salt_len somehow > hash_len!"

    #===================================================================
    # digest C - for a variable number of rounds, combine A, S, and P
    #            digests in various ways; in order to burn CPU time.
    #===================================================================

    # NOTE: the original SHA256/512-Crypt specification performs the C digest
    # calculation using the following loop:
    #
    ##dc = da
    ##i = 0
    ##while i < rounds:
    ##    tmp_ctx = hash_const(dp if i & 1 else dc)
    ##    if i % 3:
    ##        tmp_ctx.update(ds)
    ##    if i % 7:
    ##        tmp_ctx.update(dp)
    ##    tmp_ctx.update(dc if i & 1 else dp)
    ##    dc = tmp_ctx.digest()
    ##    i += 1
    #
    # The code Passlib uses (below) implements an equivalent algorithm,
    # it's just been heavily optimized to pre-calculate a large number
    # of things beforehand. It works off of a couple of observations
    # about the original algorithm:
    #
    # 1. each round is a combination of 'dc', 'ds', and 'dp'; determined
    #    by the whether 'i' a multiple of 2,3, and/or 7.
    # 2. since lcm(2,3,7)==42, the series of combinations will repeat
    #    every 42 rounds.
    # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
    #    while odd rounds 1-41 consist of hash(round-specific-constant + dc)
    #
    # Using these observations, the following code...
    # * calculates the round-specific combination of ds & dp for each round 0-41
    # * runs through as many 42-round blocks as possible
    # * runs through as many pairs of rounds as possible for remaining rounds
    # * performs once last round if the total rounds should be odd.
    #
    # this cuts out a lot of the control overhead incurred when running the
    # original loop 40,000+ times in python, resulting in ~20% increase in
    # speed under CPython (though still 2x slower than glibc crypt)

    # prepare the 6 combinations of ds & dp which are needed
    # (order of 'perms' must match how _c_digest_offsets was generated)
    dp_dp = dp+dp
    dp_ds = dp+ds
    perms = [dp, dp_dp, dp_ds, dp_ds+dp, ds+dp, ds+dp_dp]

    # build up list of even-round & odd-round constants,
    # and store in 21-element list as (even,odd) pairs.
    data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]

    # perform as many full 42-round blocks as possible
    dc = da
    blocks, tail = divmod(rounds, 42)
    while blocks:
        for even, odd in data:
            dc = hash_const(odd + hash_const(dc + even).digest()).digest()
        blocks -= 1

    # perform any leftover rounds
    if tail:
        # perform any pairs of rounds
        pairs = tail>>1
        for even, odd in data[:pairs]:
            dc = hash_const(odd + hash_const(dc + even).digest()).digest()

        # if rounds was odd, do one last round (since we started at 0,
        # last round will be an even-numbered round)
        if tail & 1:
            dc = hash_const(dc + data[pairs][0]).digest()

    #===================================================================
    # encode digest using appropriate transpose map
    #===================================================================
    return h64.encode_transposed_bytes(dc, transpose_map).decode("ascii")
예제 #28
0
파일: bcrypt.py 프로젝트: dragoncsc/HDsite
    def _norm_digest_args(cls, secret, ident, new=False):
        # make sure secret is unicode
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")

        # check max secret size
        uh.validate_secret(secret)

        # check for truncation (during .hash() calls only)
        if new:
            cls._check_truncate_policy(secret)

        # NOTE: especially important to forbid NULLs for bcrypt, since many
        # backends (bcryptor, bcrypt) happily accept them, and then
        # silently truncate the password at first NULL they encounter!
        if _BNULL in secret:
            raise uh.exc.NullPasswordError(cls)

        # TODO: figure out way to skip these tests when not needed...

        # protect from wraparound bug by truncating secret before handing it to the backend.
        # bcrypt only uses first 72 bytes anyways.
        # NOTE: not needed for 2y/2b, but might use 2a as fallback for them.
        if cls._has_2a_wraparound_bug and len(secret) >= 255:
            secret = secret[:72]

        # special case handling for variants (ordered most common first)
        if ident == IDENT_2A:
            # nothing needs to be done.
            pass

        elif ident == IDENT_2B:
            if cls._lacks_2b_support:
                # handle $2b$ hash format even if backend is too old.
                # have it generate a 2A/2Y digest, then return it as a 2B hash.
                # 2a-only backend could potentially exhibit wraparound bug --
                # but we work around that issue above.
                ident = cls._fallback_ident

        elif ident == IDENT_2Y:
            if cls._lacks_2y_support:
                # handle $2y$ hash format (not supported by BSDs, being phased out on others)
                # have it generate a 2A/2B digest, then return it as a 2Y hash.
                ident = cls._fallback_ident

        elif ident == IDENT_2:
            if cls._lacks_20_support:
                # handle legacy $2$ format (not supported by most backends except BSD os_crypt)
                # we can fake $2$ behavior using the 2A/2Y/2B algorithm
                # by repeating the password until it's at least 72 chars in length.
                if secret:
                    secret = repeat_string(secret, 72)
                ident = cls._fallback_ident

        elif ident == IDENT_2X:

            # NOTE: shouldn't get here.
            # XXX: could check if backend does actually offer 'support'
            raise RuntimeError("$2x$ hashes not currently supported by passlib")

        else:
            raise AssertionError("unexpected ident value: %r" % ident)

        return secret, ident
예제 #29
0
class _bcrypt_sha256_test(HandlerCase):
    "base for BCrypt-SHA256 test cases"
    handler = hash.bcrypt_sha256
    reduce_default_rounds = True
    forbidden_characters = None
    fuzz_salts_need_bcrypt_repair = True
    fallback_os_crypt_handler = hash.bcrypt

    known_correct_hashes = [
        #
        # custom test vectors
        #

        # empty
        ("",
         '$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'
         ),

        # ascii
        ("password",
         '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'
         ),

        # unicode / utf8
        (UPASS_TABLE,
         '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'
         ),
        (UPASS_TABLE.encode("utf-8"),
         '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'
         ),

        # test >72 chars is hashed correctly -- under bcrypt these hash the same.
        # NOTE: test_60_secret_size() handles this already, this is just for overkill :)
        (repeat_string("abc123", 72),
         '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'
         ),
        (repeat_string("abc123", 72) + "qwr",
         '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'
         ),
        (repeat_string("abc123", 72) + "xyz",
         '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'
         ),
    ]

    known_correct_configs = [
        ('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe', "password",
         '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'
         ),
    ]

    known_malformed_hashes = [
        # bad char in otherwise correct hash
        #                           \/
        '$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unrecognized bcrypt variant
        '$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unsupported bcrypt variant
        '$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # rounds zero-padded
        '$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # config string w/ $ added
        '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',
    ]

    #===================================================================
    # override some methods -- cloned from bcrypt
    #===================================================================
    def setUp(self):
        # ensure builtin is enabled for duration of test.
        if TEST_MODE("full") and self.backend == "builtin":
            key = "PASSLIB_BUILTIN_BCRYPT"
            orig = os.environ.get(key)
            if orig:
                self.addCleanup(os.environ.__setitem__, key, orig)
            else:
                self.addCleanup(os.environ.__delitem__, key)
            os.environ[key] = "enabled"
        super(_bcrypt_sha256_test, self).setUp()

    def populate_settings(self, kwds):
        # builtin is still just way too slow.
        if self.backend == "builtin":
            kwds.setdefault("rounds", 4)
        super(_bcrypt_sha256_test, self).populate_settings(kwds)

    #===================================================================
    # override ident tests for now
    #===================================================================
    def test_30_HasManyIdents(self):
        raise self.skipTest("multiple idents not supported")

    def test_30_HasOneIdent(self):
        # forbidding ident keyword, we only support "2a" for now
        handler = self.handler
        handler(use_defaults=True)
        self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)

    #===================================================================
    # fuzz testing -- cloned from bcrypt
    #===================================================================
    def fuzz_setting_rounds(self):
        # decrease default rounds for fuzz testing to speed up volume.
        return randintgauss(5, 8, 6, 1)
예제 #30
0
def _raw_md5_crypt(pwd, salt, use_apr=False):
    """perform raw md5-crypt calculation

    this function provides a pure-python implementation of the internals
    for the MD5-Crypt algorithms; it doesn't handle any of the
    parsing/validation of the hash strings themselves.

    :arg pwd: password chars/bytes to encrypt
    :arg salt: salt chars to use
    :arg use_apr: use apache variant

    :returns:
        encoded checksum chars
    """
    # NOTE: regarding 'apr' format:
    # really, apache? you had to invent a whole new "$apr1$" format,
    # when all you did was change the ident incorporated into the hash?
    # would love to find webpage explaining why just using a portable
    # implementation of $1$ wasn't sufficient. *nothing else* was changed.

    #===================================================================
    # init & validate inputs
    #===================================================================

    # validate secret
    # XXX: not sure what official unicode policy is, using this as default
    if isinstance(pwd, unicode):
        pwd = pwd.encode("utf-8")
    assert isinstance(pwd, bytes), "pwd not unicode or bytes"
    if _BNULL in pwd:
        raise uh.exc.NullPasswordError(md5_crypt)
    pwd_len = len(pwd)

    # validate salt - should have been taken care of by caller
    assert isinstance(salt, unicode), "salt not unicode"
    salt = salt.encode("ascii")
    assert len(salt) < 9, "salt too large"
        # NOTE: spec says salts larger than 8 bytes should be truncated,
        # instead of causing an error. this function assumes that's been
        # taken care of by the handler class.

    # load APR specific constants
    if use_apr:
        magic = _APR_MAGIC
    else:
        magic = _MD5_MAGIC

    #===================================================================
    # digest B - used as subinput to digest A
    #===================================================================
    db = md5(pwd + salt + pwd).digest()

    #===================================================================
    # digest A - used to initialize first round of digest C
    #===================================================================
    # start out with pwd + magic + salt
    a_ctx = md5(pwd + magic + salt)
    a_ctx_update = a_ctx.update

    # add pwd_len bytes of b, repeating b as many times as needed.
    a_ctx_update(repeat_string(db, pwd_len))

    # add null chars & first char of password
        # NOTE: this may have historically been a bug,
        # where they meant to use db[0] instead of B_NULL,
        # but the original code memclear'ed db,
        # and now all implementations have to use this.
    i = pwd_len
    evenchar = pwd[:1]
    while i:
        a_ctx_update(_BNULL if i & 1 else evenchar)
        i >>= 1

    # finish A
    da = a_ctx.digest()

    #===================================================================
    # digest C - for a 1000 rounds, combine A, S, and P
    #            digests in various ways; in order to burn CPU time.
    #===================================================================

    # NOTE: the original MD5-Crypt implementation performs the C digest
    # calculation using the following loop:
    #
    ##dc = da
    ##i = 0
    ##while i < rounds:
    ##    tmp_ctx = md5(pwd if i & 1 else dc)
    ##    if i % 3:
    ##        tmp_ctx.update(salt)
    ##    if i % 7:
    ##        tmp_ctx.update(pwd)
    ##    tmp_ctx.update(dc if i & 1 else pwd)
    ##    dc = tmp_ctx.digest()
    ##    i += 1
    #
    # The code Passlib uses (below) implements an equivalent algorithm,
    # it's just been heavily optimized to pre-calculate a large number
    # of things beforehand. It works off of a couple of observations
    # about the original algorithm:
    #
    # 1. each round is a combination of 'dc', 'salt', and 'pwd'; and the exact
    #    combination is determined by whether 'i' a multiple of 2,3, and/or 7.
    # 2. since lcm(2,3,7)==42, the series of combinations will repeat
    #    every 42 rounds.
    # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
    #    while odd rounds 1-41 consist of hash(round-specific-constant + dc)
    #
    # Using these observations, the following code...
    # * calculates the round-specific combination of salt & pwd for each round 0-41
    # * runs through as many 42-round blocks as possible (23)
    # * runs through as many pairs of rounds as needed for remaining rounds (17)
    # * this results in the required 42*23+2*17=1000 rounds required by md5_crypt.
    #
    # this cuts out a lot of the control overhead incurred when running the
    # original loop 1000 times in python, resulting in ~20% increase in
    # speed under CPython (though still 2x slower than glibc crypt)

    # prepare the 6 combinations of pwd & salt which are needed
    # (order of 'perms' must match how _c_digest_offsets was generated)
    pwd_pwd = pwd+pwd
    pwd_salt = pwd+salt
    perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd]

    # build up list of even-round & odd-round constants,
    # and store in 21-element list as (even,odd) pairs.
    data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]

    # perform 23 blocks of 42 rounds each (for a total of 966 rounds)
    dc = da
    blocks = 23
    while blocks:
        for even, odd in data:
            dc = md5(odd + md5(dc + even).digest()).digest()
        blocks -= 1

    # perform 17 more pairs of rounds (34 more rounds, for a total of 1000)
    for even, odd in data[:17]:
        dc = md5(odd + md5(dc + even).digest()).digest()

    #===================================================================
    # encode digest using appropriate transpose map
    #===================================================================
    return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii")
예제 #31
0
class _bcrypt_sha256_test(HandlerCase):
    "base for BCrypt-SHA256 test cases"
    handler = hash.bcrypt_sha256
    reduce_default_rounds = True
    forbidden_characters = None
    fuzz_salts_need_bcrypt_repair = True

    known_correct_hashes = [
        # -------------------------------------------------------------------
        # custom test vectors for old v1 format
        # -------------------------------------------------------------------

        # empty
        ("",
         '$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'
         ),

        # ascii
        ("password",
         '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'
         ),

        # unicode / utf8
        (UPASS_TABLE,
         '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'
         ),
        (UPASS_TABLE.encode("utf-8"),
         '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'
         ),

        # ensure 2b support
        ("password",
         '$bcrypt-sha256$2b,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'
         ),
        (UPASS_TABLE,
         '$bcrypt-sha256$2b,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'
         ),

        # test >72 chars is hashed correctly -- under bcrypt these hash the same.
        # NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
        (repeat_string("abc123", 72),
         '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'
         ),
        (repeat_string("abc123", 72) + "qwr",
         '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'
         ),
        (repeat_string("abc123", 72) + "xyz",
         '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'
         ),

        # -------------------------------------------------------------------
        # custom test vectors for v2 format
        # TODO: convert to v2 format
        # -------------------------------------------------------------------

        # empty
        ("",
         '$bcrypt-sha256$v=2,t=2b,r=5$E/e/2AOhqM5W/KJTFQzLce$WFPIZKtDDTriqWwlmRFfHiOTeheAZWe'
         ),

        # ascii
        ("password",
         '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'
         ),

        # unicode / utf8
        (UPASS_TABLE,
         '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'
         ),
        (UPASS_TABLE.encode("utf-8"),
         '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'
         ),

        # test >72 chars is hashed correctly -- under bcrypt these hash the same.
        # NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
        (repeat_string("abc123", 72),
         '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zu1cloESVFIOsUIo7fCEgkdHaI9SSue'
         ),
        (repeat_string("abc123", 72) + "qwr",
         '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$CBF9csfEdW68xv3DwE6xSULXMtqEFP.'
         ),
        (repeat_string("abc123", 72) + "xyz",
         '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zC/1UDUG2ofEXB6Onr2vvyFzfhEOS3S'
         ),
    ]

    known_correct_configs = [
        # v1
        ('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe', "password",
         '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'
         ),
        # v2
        ('$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe', "password",
         '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'
         ),
    ]

    known_malformed_hashes = [
        # -------------------------------------------------------------------
        # v1 format
        # -------------------------------------------------------------------

        # bad char in otherwise correct hash
        #                           \/
        '$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unrecognized bcrypt variant
        '$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unsupported bcrypt variant
        '$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # rounds zero-padded
        '$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # config string w/ $ added
        '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',

        # -------------------------------------------------------------------
        # v2 format
        # -------------------------------------------------------------------

        # bad char in otherwise correct hash
        #                                   \/
        '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unsupported version (for this format)
        '$bcrypt-sha256$v=1,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unrecognized version
        '$bcrypt-sha256$v=3,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unrecognized bcrypt variant
        '$bcrypt-sha256$v=2,t=2c,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # unsupported bcrypt variant
        '$bcrypt-sha256$v=2,t=2a,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
        '$bcrypt-sha256$v=2,t=2x,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # rounds zero-padded
        '$bcrypt-sha256$v=2,t=2b,r=05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',

        # config string w/ $ added
        '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$',
    ]

    # ===================================================================
    # override some methods -- cloned from bcrypt
    # ===================================================================
    def setUp(self):
        # ensure builtin is enabled for duration of test.
        if TEST_MODE("full") and self.backend == "builtin":
            key = "PASSLIB_BUILTIN_BCRYPT"
            orig = os.environ.get(key)
            if orig:
                self.addCleanup(os.environ.__setitem__, key, orig)
            else:
                self.addCleanup(os.environ.__delitem__, key)
            os.environ[key] = "enabled"
        super(_bcrypt_sha256_test, self).setUp()
        warnings.filterwarnings(
            "ignore", ".*backend is vulnerable to the bsd wraparound bug.*")

    def populate_settings(self, kwds):
        # builtin is still just way too slow.
        if self.backend == "builtin":
            kwds.setdefault("rounds", 4)
        super(_bcrypt_sha256_test, self).populate_settings(kwds)

    # ===================================================================
    # override ident tests for now
    # ===================================================================

    def require_many_idents(self):
        raise self.skipTest("multiple idents not supported")

    def test_30_HasOneIdent(self):
        # forbidding ident keyword, we only support "2b" for now
        handler = self.handler
        handler(use_defaults=True)
        self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)

    # ===================================================================
    # fuzz testing -- cloned from bcrypt
    # ===================================================================

    class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
        def random_rounds(self):
            # decrease default rounds for fuzz testing to speed up volume.
            return self.randintgauss(5, 8, 6, 1)

        def random_ident(self):
            return "2b"

    # ===================================================================
    # custom tests
    # ===================================================================

    def test_using_version(self):
        # default to v2
        handler = self.handler
        self.assertEqual(handler.version, 2)

        # allow v1 explicitly
        subcls = handler.using(version=1)
        self.assertEqual(subcls.version, 1)

        # forbid unknown ver
        self.assertRaises(ValueError, handler.using, version=999)

        # allow '2a' only for v1
        subcls = handler.using(version=1, ident="2a")
        self.assertRaises(ValueError, handler.using, ident="2a")

    def test_calc_digest_v2(self):
        """
        test digest calc v2 matches bcrypt()
        """
        from passlib.hash import bcrypt
        from passlib.crypto.digest import compile_hmac
        from passlib.utils.binary import b64encode

        # manually calc intermediary digest
        salt = "nyKYxTAvjmy6lMDYMl11Uu"
        secret = "test"
        temp_digest = compile_hmac("sha256", salt.encode("ascii"))(
            secret.encode("ascii"))
        temp_digest = b64encode(temp_digest).decode("ascii")
        self.assertEqual(temp_digest,
                         "J5TlyIDm+IcSWmKiDJm+MeICndBkFVPn4kKdJW8f+xY=")

        # manually final hash from intermediary
        # XXX: genhash() could be useful here
        bcrypt_digest = bcrypt(ident="2b", salt=salt,
                               rounds=12)._calc_checksum(temp_digest)
        self.assertEqual(bcrypt_digest, "M0wE0Ov/9LXoQFCe.jRHu3MSHPF54Ta")
        self.assertTrue(
            bcrypt.verify(temp_digest, "$2b$12$" + salt + bcrypt_digest))

        # confirm handler outputs same thing.
        # XXX: genhash() could be useful here
        result = self.handler(ident="2b", salt=salt,
                              rounds=12)._calc_checksum(secret)
        self.assertEqual(result, bcrypt_digest)