diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-04-13 14:10:11 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-04-13 14:10:11 -0400 |
commit | 5a3bd0d6ac8ad706c7d4a21aa49a51c9fcc54873 (patch) | |
tree | 185ad46852f88a753335d2c7bf662d0e0ef0c288 /passlib/handlers/des_crypt.py | |
parent | c0f420bf7d7659ee110432f7cbb0233554dfd32a (diff) | |
download | passlib-5a3bd0d6ac8ad706c7d4a21aa49a51c9fcc54873.tar.gz |
work on des_crypt family
* cleaned up source of des_crypt variants and DES util functions
* DES utils functions now have tighter input validation, full UT coverage
Diffstat (limited to 'passlib/handlers/des_crypt.py')
-rw-r--r-- | passlib/handlers/des_crypt.py | 213 |
1 files changed, 77 insertions, 136 deletions
diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py index 56102c0..d87c495 100644 --- a/passlib/handlers/des_crypt.py +++ b/passlib/handlers/des_crypt.py @@ -1,54 +1,4 @@ -"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants - -.. note:: - - for des-crypt, passlib restricts salt characters to just the hash64 charset, - and salt string size to >= 2 chars; since implementations of des-crypt - vary in how they handle other characters / sizes... - - linux - - linux crypt() accepts salt characters outside the hash64 charset, - and maps them using the following formula (determined by examining crypt's output): - chr 0..64: v = (c-(1-19)) & 63 = (c+18) & 63 - chr 65..96: v = (c-(65-12)) & 63 = (c+11) & 63 - chr 97..127: v = (c-(97-38)) & 63 = (c+5) & 63 - chr 128..255: same as c-128 - - invalid salt chars are mirrored back in the resulting hash. - - if the salt is too small, it uses a NUL char for the remaining - character (which is treated the same as the char ``G``) - when decoding the 12 bit salt. however, it outputs - a hash string containing the single salt char twice, - resulting in a corrupted hash. - - netbsd - - netbsd crypt() uses a 128-byte lookup table, - which is only initialized for the hash64 values. - the remaining values < 128 are implicitly zeroed, - and values > 128 access past the array bounds - (but seem to return 0). - - if the salt string is too small, it reads - the NULL char (and continues past the end for bsdi crypt, - though the buffer is usually large enough and NULLed). - salt strings are output as provided, - except for any NULs, which are converted to ``.``. - - openbsd, freebsd - - openbsd crypt() strictly defines the hash64 values as normal, - and all other char values as 0. salt chars are reported as provided. - - if the salt or rounds string is too small, - it'll read past the end, resulting in unpredictable - values, though it'll terminate it's encoding - of the output at the first null. - this will generally result in a corrupted hash. -""" - +"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants""" #========================================================= #imports #========================================================= @@ -60,7 +10,7 @@ from warnings import warn #libs from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt, to_unicode from passlib.utils.compat import b, bytes, byte_elem_value, u, uascii_to_str, unicode -from passlib.utils.des import mdes_encrypt_int_block +from passlib.utils.des import des_encrypt_int_block import passlib.utils.handlers as uh #pkg #local @@ -72,70 +22,86 @@ __all__ = [ ] #========================================================= -#pure-python backend +# pure-python backend for des_crypt family #========================================================= def _crypt_secret_to_key(secret): - "crypt helper which converts lower 7 bits of first 8 chars of secret -> 56-bit des key, padded to 64 bits" - return sum( - (byte_elem_value(c) & 0x7f) << (57-8*i) - for i, c in enumerate(secret[:8]) - ) - -def raw_crypt(secret, salt): - "pure-python fallback if stdlib support not present" + """convert secret to 64-bit DES key. + + this only uses the first 8 bytes of the secret, + and discards the high 8th bit of each byte at that. + a null parity bit is inserted after every 7th bit of the output. + """ + # NOTE: this would set the parity bits correctly, + # but des_encrypt_int_block() would just ignore them... + ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8) + ## for i, c in enumerate(secret[:8])) + return sum((byte_elem_value(c) & 0x7f) << (57-i*8) + for i, c in enumerate(secret[:8])) + +def _raw_des_crypt(secret, salt): + "pure-python backed for des_crypt" assert len(salt) == 2 - #NOTE: technically could accept non-standard salts & single char salt, - #but no official spec. + # NOTE: some OSes will accept non-HASH64 characters in the salt, + # but what value they assign these characters varies wildy, + # so just rejecting them outright. + # NOTE: the same goes for single-character salts... + # some OSes duplicate the char, some insert a '.' char, + # and openbsd does something which creates an invalid hash. try: salt_value = h64.decode_int12(salt) except ValueError: #pragma: no cover - always caught by class raise ValueError("invalid chars in salt") - #FIXME: ^ this will throws error if bad salt chars are used - # whereas linux crypt does something (inexplicable) with it - #convert first 8 bytes of secret string into an integer + # forbidding NULL char because underlying crypt() rejects them too. + if b('\x00') in secret: + raise ValueError("null char in secret") + + # convert first 8 bytes of secret string into an integer key_value = _crypt_secret_to_key(secret) - #run data through des using input of 0 - result = mdes_encrypt_int_block(key_value, 0, salt_value, 25) + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, 25) - #run h64 encode on result + # run h64 encode on result return h64big.encode_int64(result) -def raw_ext_crypt(secret, rounds, salt): - "ext_crypt() helper which returns checksum only" +def _bsdi_secret_to_key(secret): + "covert secret to DES key used by bsdi_crypt" + key_value = _crypt_secret_to_key(secret) + idx = 8 + end = len(secret) + while idx < end: + next = idx+8 + tmp_value = _crypt_secret_to_key(secret[idx:next]) + key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value + idx = next + return key_value - #decode salt +def _raw_bsdi_crypt(secret, rounds, salt): + "pure-python backend for bsdi_crypt" + + # decode salt try: salt_value = h64.decode_int24(salt) except ValueError: #pragma: no cover - always caught by class raise ValueError("invalid salt") - #validate secret - if b('\x00') in secret: #pragma: no cover - always caught by class - #builtin linux crypt doesn't like this, so we don't either - #XXX: would make more sense to raise ValueError, but want to be compatible w/ stdlib crypt + # forbidding NULL char because underlying crypt() rejects them too. + if b('\x00') in secret: raise ValueError("secret must be string without null bytes") - #convert secret string into an integer - key_value = _crypt_secret_to_key(secret) - idx = 8 - end = len(secret) - while idx < end: - next = idx+8 - key_value = mdes_encrypt_int_block(key_value, key_value) ^ \ - _crypt_secret_to_key(secret[idx:next]) - idx = next + # convert secret string into an integer + key_value = _bsdi_secret_to_key(secret) - #run data through des using input of 0 - result = mdes_encrypt_int_block(key_value, 0, salt_value, rounds) + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, rounds) - #run h64 encode on result + # run h64 encode on result return h64big.encode_int64(result) #========================================================= -#handler +# handlers #========================================================= class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`. @@ -156,9 +122,8 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): You can see which backend is in use by calling the :meth:`get_backend()` method. """ - #========================================================= - #class attrs + # class attrs #========================================================= #--GenericHandler-- name = "des_crypt" @@ -170,7 +135,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): salt_chars = uh.HASH64_CHARS #========================================================= - #formatting + # formatting #========================================================= #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum @@ -191,7 +156,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): return uascii_to_str(hash) #========================================================= - #backend + # backend #========================================================= backends = ("os_crypt", "builtin") @@ -205,11 +170,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): # gotta do something - no official policy since des-crypt predates unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") - # forbidding nul chars because linux crypt (and most C implementations) - # won't accept it either. - if b('\x00') in secret: - raise ValueError("null char in secret") - return raw_crypt(secret, self.salt.encode("ascii")).decode("ascii") + return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): # NOTE: safe_crypt encodes unicode secret -> utf8 @@ -222,19 +183,9 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): return self._calc_checksum_builtin(secret) #========================================================= - #eoc + # eoc #========================================================= -#========================================================= -#handler -#========================================================= - -#FIXME: phpass code notes that even rounds values should be avoided for BSDI-Crypt, -# so as not to reveal weak des keys. given the random salt, this shouldn't be -# a very likely issue anyways, but should do something about default rounds generation anyways. -# http://wiki.call-cc.org/eggref/4/crypt sez even rounds of DES may reveal weak keys. -# list of semi-weak keys - http://dolphinburger.com/cgi-bin/bsdi-man?proto=1.1&query=bdes&msection=1&apropos=0 - class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`. @@ -259,7 +210,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler You can see which backend is in use by calling the :meth:`get_backend()` method. """ #========================================================= - #class attrs + # class attrs #========================================================= #--GenericHandler-- name = "bsdi_crypt" @@ -281,7 +232,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler # but that seems to be an OS policy, not a algorithm limitation. #========================================================= - #internal helpers + # parsing #========================================================= _hash_regex = re.compile(u(r""" ^ @@ -323,7 +274,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - return raw_ext_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") + return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): config = self.to_string() @@ -335,12 +286,9 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler return self._calc_checksum_builtin(secret) #========================================================= - #eoc + # eoc #========================================================= -#========================================================= -# -#========================================================= class bigcrypt(uh.HasSalt, uh.GenericHandler): """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`. @@ -354,7 +302,7 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. """ #========================================================= - #class attrs + # class attrs #========================================================= #--GenericHandler-- name = "bigcrypt" @@ -367,7 +315,7 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): salt_chars = uh.HASH64_CHARS #========================================================= - #internal helpers + # internal helpers #========================================================= _hash_regex = re.compile(u(r""" ^ @@ -395,29 +343,24 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): return value #========================================================= - #backend + # backend #========================================================= - #TODO: check if os_crypt supports ext-des-crypt. - def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - chk = raw_crypt(secret, self.salt.encode("ascii")) + chk = _raw_des_crypt(secret, self.salt.encode("ascii")) idx = 8 end = len(secret) while idx < end: next = idx + 8 - chk += raw_crypt(secret[idx:next], chk[-11:-9]) + chk += _raw_des_crypt(secret[idx:next], chk[-11:-9]) idx = next return chk.decode("ascii") #========================================================= - #eoc + # eoc #========================================================= -#========================================================= -# -#========================================================= class crypt16(uh.HasSalt, uh.GenericHandler): """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`. @@ -431,7 +374,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler): If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. """ #========================================================= - #class attrs + # class attrs #========================================================= #--GenericHandler-- name = "crypt16" @@ -444,7 +387,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler): salt_chars = uh.HASH64_CHARS #========================================================= - #internal helpers + # internal helpers #========================================================= _hash_regex = re.compile(u(r""" ^ @@ -466,10 +409,8 @@ class crypt16(uh.HasSalt, uh.GenericHandler): return uascii_to_str(hash) #========================================================= - #backend + # backend #========================================================= - #TODO: check if os_crypt supports ext-des-crypt. - def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") @@ -484,22 +425,22 @@ class crypt16(uh.HasSalt, uh.GenericHandler): key1 = _crypt_secret_to_key(secret) #run data through des using input of 0 - result1 = mdes_encrypt_int_block(key1, 0, salt_value, 20) + result1 = des_encrypt_int_block(key1, 0, salt_value, 20) #convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars) key2 = _crypt_secret_to_key(secret[8:16]) #run data through des using input of 0 - result2 = mdes_encrypt_int_block(key2, 0, salt_value, 5) + result2 = des_encrypt_int_block(key2, 0, salt_value, 5) #done chk = h64big.encode_int64(result1) + h64big.encode_int64(result2) return chk.decode("ascii") #========================================================= - #eoc + # eoc #========================================================= #========================================================= -#eof +# eof #========================================================= |