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
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
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)
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
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
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
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
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)
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)
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,))
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:])
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:])
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, ))
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
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")
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")
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
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))
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_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 _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")
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")
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
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")
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")
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")
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
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)
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")
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)