summaryrefslogtreecommitdiff
path: root/passlib/handlers/des_crypt.py
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-13 14:10:11 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-13 14:10:11 -0400
commit5a3bd0d6ac8ad706c7d4a21aa49a51c9fcc54873 (patch)
tree185ad46852f88a753335d2c7bf662d0e0ef0c288 /passlib/handlers/des_crypt.py
parentc0f420bf7d7659ee110432f7cbb0233554dfd32a (diff)
downloadpasslib-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.py213
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
#=========================================================