diff options
| -rw-r--r-- | .hgignore | 2 | ||||
| -rw-r--r-- | docs/conf.py | 6 | ||||
| -rw-r--r-- | docs/lib/passlib.hash.md5_crypt.rst | 2 | ||||
| -rw-r--r-- | docs/lib/passlib.hash.nthash.rst | 35 | ||||
| -rw-r--r-- | docs/lib/passlib.hash.phpass.rst | 12 | ||||
| -rw-r--r-- | docs/lib/passlib.utils.h64.rst | 14 | ||||
| -rw-r--r-- | docs/lib/passlib.utils.rst | 2 | ||||
| -rw-r--r-- | passlib/hash/nthash.py | 99 | ||||
| -rw-r--r-- | passlib/hash/phpass.py | 2 | ||||
| -rw-r--r-- | passlib/tests/handler_utils.py | 6 | ||||
| -rw-r--r-- | passlib/tests/test_hash_misc.py (renamed from passlib/tests/test_hash_phpass.py) | 25 | ||||
| -rw-r--r-- | passlib/utils/h64.py | 45 | ||||
| -rw-r--r-- | passlib/win32.py | 12 |
13 files changed, 219 insertions, 43 deletions
diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..4abcf8b --- /dev/null +++ b/.hgignore @@ -0,0 +1,2 @@ +relre:docs/_build +glob:*.pyc diff --git a/docs/conf.py b/docs/conf.py index 206a63a..e6b92cb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,8 @@ import os, sys +options = os.environ.get("PASSLIB_DOCS", "") + #make sure passlib in sys.path doc_root = os.path.abspath(os.path.join(__file__,os.path.pardir)) source_root = os.path.abspath(os.path.join(doc_root,os.path.pardir)) @@ -109,8 +111,8 @@ pygments_style = 'sphinx' modindex_common_prefix = [ "passlib." ] # -- Options for all output --------------------------------------------------- -todo_include_todos = "todos" in os.environ.get("PASSLIB_DOCS","") -keep_warnings = True +todo_include_todos = "hide-todos" not in options +keep_warnings = "hide-warnings" not in options # -- Options for HTML output --------------------------------------------------- diff --git a/docs/lib/passlib.hash.md5_crypt.rst b/docs/lib/passlib.hash.md5_crypt.rst index 76c0b96..4fb9db9 100644 --- a/docs/lib/passlib.hash.md5_crypt.rst +++ b/docs/lib/passlib.hash.md5_crypt.rst @@ -3,7 +3,7 @@ ================================================================== .. module:: passlib.hash.md5_crypt - :synopsis: MD5-Crypt + :synopsis: MD5 Crypt This algorithm was developed to replace the aging des-crypt crypt. It is supported by a wide variety of unix flavors, and is found diff --git a/docs/lib/passlib.hash.nthash.rst b/docs/lib/passlib.hash.nthash.rst index a8885b5..bc6a6b1 100644 --- a/docs/lib/passlib.hash.nthash.rst +++ b/docs/lib/passlib.hash.nthash.rst @@ -9,14 +9,32 @@ This scheme is notoriously weak (since it's based on :mod:`~passlib.utils.md4`). Online tables exist for quickly performing pre-image attacks on this scheme. - **Do not use** in new code. + **Do not use** in new code. Stop using in old code if possible. -This handler implements the Windows NT-HASH algorithm, -encoded in a format compatible with the :ref:`modular-crypt-format`. +This module implements the Windows NT-HASH algorithm, +encoded in a manner compatible with the :ref:`modular-crypt-format`. It is found on some unix systems where the administrator has decided to store user passwords in a manner compatible with the SMB/CIFS protocol. -It supports two identifiers, ``$3$`` and ``$NT$``, though it defaults to ``$3$``. +It supports two identifiers, ``$3$`` and ``$NT$``, though this +implementation defaults to ``$3$``. + +It has no salt, or variable rounds. + +Usage +===== + +.. todo:: + + document usage + +Functions +========= +.. autofunction:: genconfig +.. autofunction:: genhash +.. autofunction:: encrypt +.. autofunction:: identify +.. autofunction:: verify In addition to the normal password hash api, this module also exposes the following method: @@ -25,3 +43,12 @@ the following method: perform raw nthash calculation, returning either raw digest, or as lower-case hexidecimal characters. + +Format & Algorithm +================== +A nthash encoded for crypt consists of ``$3$$<checksum>`` or +``$NT$<checksum>``; where ``checksum`` is 32 hexidecimal digits +encoding the checksum. An example hash (of ``password``) is ``$3$$8846f7eaee8fb117ad06bdd830b7586c``. + +The checksum is simply the :mod:`~passlib.utils.md4` digest +of the secret using the ``UTF16-LE`` encoding. diff --git a/docs/lib/passlib.hash.phpass.rst b/docs/lib/passlib.hash.phpass.rst index 3808501..8004f39 100644 --- a/docs/lib/passlib.hash.phpass.rst +++ b/docs/lib/passlib.hash.phpass.rst @@ -29,8 +29,8 @@ Functions .. autofunction:: identify .. autofunction:: verify -Format -====== +Format & Algorithm +================== An phpass portable hash string has length 34, with the format ``$P$<rounds><salt><checksum>``; where ``<rounds>`` is a single character encoding a 6-bit integer, ``<salt>`` is an eight-character salt, and ``<checksum>`` is an encoding @@ -40,12 +40,10 @@ An example hash (of ``password``) is ``$P$8ohUJ.1sdFw09/bMaAQPTGDNi2BIUt1``; the rounds are encoded in ``8``, the salt is ``ohUJ.1sd``, and the checksum is ``Fw09/bMaAQPTGDNi2BIUt1``. -Algorithm -========= -PHPass uses a straightforward algorithm: +PHPass uses a straightforward algorithm to calculate the checksum: * an initial result is generated from the MD5 digest of the salt string + the secret. -* for 2**rounds repetitions, a new result is created from the MD5 digest of the last result + the secret. +* for ``2**rounds`` repetitions, a new result is created from the MD5 digest of the last result + the secret. * the last result is then encoded according to the format described above. Deviations @@ -58,4 +56,4 @@ This implementation of phpass differs from the specification: References ========== -* `<http://www.openwall.com/phpass/>` - PHPass homepage, which describes the algorithm +* `<http://www.openwall.com/phpass/>`_ - PHPass homepage, which describes the algorithm diff --git a/docs/lib/passlib.utils.h64.rst b/docs/lib/passlib.utils.h64.rst index 9709ea1..548720c 100644 --- a/docs/lib/passlib.utils.h64.rst +++ b/docs/lib/passlib.utils.h64.rst @@ -22,15 +22,27 @@ and decoding strings in that format. when in fact bcrypt uses the standard base64 encoding scheme, but with ``+`` replaced with ``.``. -.. data:: CHARS +Constants +========= +.. object:: CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" The character set used by the Hash-64 format. A character's index in CHARS denotes it's corresponding 6-bit integer value. +Bytes <-> Hash64 +================ + +.. autofunction:: encode_bytes .. autofunction:: encode_3_offsets .. autofunction:: encode_2_offsets .. autofunction:: encode_1_offset +Int <-> Hash64 +============== + +.. autofunction:: decode_int6 +.. autofunction:: encode_int6 + .. autofunction:: decode_int12 .. autofunction:: encode_int12 diff --git a/docs/lib/passlib.utils.rst b/docs/lib/passlib.utils.rst index 3c6a627..91788f4 100644 --- a/docs/lib/passlib.utils.rst +++ b/docs/lib/passlib.utils.rst @@ -46,7 +46,7 @@ Object Tests .. todo:: - .. autofunction:: is_crypt_context + is_crypt_context Crypt Handler Helpers ===================== diff --git a/passlib/hash/nthash.py b/passlib/hash/nthash.py new file mode 100644 index 0000000..543122a --- /dev/null +++ b/passlib/hash/nthash.py @@ -0,0 +1,99 @@ +"""passlib.hash.nthash - unix-crypt compatible nthash passwords""" +#========================================================= +#imports +#========================================================= +#core +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +#site +#libs +from passlib.utils.md4 import md4 +#pkg +#local +__all__ = [ + "genhash", + "genconfig", + "encrypt", + "identify", + "verify", +] + +#========================================================= +#backend +#========================================================= +def raw_nthash(secret, hex=False): + "encode password using md4-based NTHASH algorithm; returns string of raw bytes" + hash = md4(secret.encode("utf-16le")) + return hash.hexdigest() if hex else hash.digest() + +#========================================================= +#algorithm information +#========================================================= +name = "nthash" +#stats: 128 bit checksum, no salt + +setting_kwds = () +context_kwds = () + +#========================================================= +#internal helpers +#========================================================= +_pat = re.compile(r""" + ^ + \$(?P<ident>3\$\$|NT\$) + (?P<chk>[a-f0-9]{32}) + $ + """, re.X) + +def parse(hash): + if not hash: + raise ValueError, "no hash specified" + m = _pat.match(hash) + if not m: + raise ValueError, "invalid nthash" + ident, chk = m.group("ident", "chk") + out = dict( + checksum=chk, + ) + ident=ident.strip("$") + if ident != "3": + out['ident'] = ident + return out + +def render(checksum, ident=None): + if not ident or ident == "3": + return "$3$$" + checksum + elif ident == "NT": + return "$NT$" + checksum + else: + raise ValueError, "invalid ident" + +#========================================================= +#primary interface +#========================================================= +def genconfig(ident=None): + return render("0" * 32, ident) + +def genhash(secret, config): + info = parse(config) + if secret is None: + raise TypeError, "secret must be a string" + chk = raw_nthash(secret, hex=True) + return render(chk, info.get('ident')) + +#========================================================= +#secondary interface +#========================================================= +def encrypt(secret, **settings): + return genhash(secret, genconfig(**settings)) + +def verify(secret, hash): + return hash == genhash(secret, hash) + +def identify(hash): + return bool(hash and _pat.match(hash)) + +#========================================================= +#eof +#========================================================= diff --git a/passlib/hash/phpass.py b/passlib/hash/phpass.py index 5be4708..b161073 100644 --- a/passlib/hash/phpass.py +++ b/passlib/hash/phpass.py @@ -100,7 +100,7 @@ def genconfig(salt=None, rounds=None, ident="P"): :param ident: phpBB3 uses ``H`` instead of ``P`` for it's identifier. - this may be set to generate phpBB3 compatible hashes. + this may be set to ``H`` in order to generate phpBB3 compatible hashes. :returns: phpass configuration string. diff --git a/passlib/tests/handler_utils.py b/passlib/tests/handler_utils.py index e88e73c..f6e1981 100644 --- a/passlib/tests/handler_utils.py +++ b/passlib/tests/handler_utils.py @@ -241,7 +241,7 @@ class _HandlerTestCase(TestCase): "test secret_chars limit" sc = self.secret_chars - base = "too many secrets" + base = "too many secrets" #16 chars alt = 'x' #char that's not in base string if sc > 0: @@ -268,7 +268,9 @@ class _HandlerTestCase(TestCase): #NOTE: this doesn't do an exhaustive search to verify algorithm #doesn't have some cutoff point, it just tries - #1024-character string, and alters the last char + #1024-character string, and alters the last char. + #as long as algorithm doesn't clip secret at point <1024, + #the new secret shouldn't verify. secret = base * 64 hash = self.do_encrypt(secret) self.assert_(not self.do_verify(secret[:-1] + alt, hash)) diff --git a/passlib/tests/test_hash_phpass.py b/passlib/tests/test_hash_misc.py index af90bd8..da3d0ba 100644 --- a/passlib/tests/test_hash_phpass.py +++ b/passlib/tests/test_hash_misc.py @@ -10,15 +10,16 @@ from logging import getLogger #pkg from passlib.tests.handler_utils import _HandlerTestCase from passlib.tests.utils import enable_option -import passlib.hash.phpass as mod #module log = getLogger(__name__) #========================================================= -#md5 crypt +#PHPass Portable Crypt #========================================================= +from passlib.hash import phpass + class PHPassTest(_HandlerTestCase): - handler = mod + handler = phpass known_correct = ( ('', '$P$7JaFQsPzJSuenezefD/3jHgt5hVfNH0'), @@ -32,5 +33,23 @@ class PHPassTest(_HandlerTestCase): ) #========================================================= +#NTHASH for unix +#========================================================= +from passlib.hash import nthash + +class NTHashTest(_HandlerTestCase): + handler = nthash + + known_correct = ( + ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'), + ('passphrase', '$NT$7f8fe03093cc84b267b109625f6bbf4b'), + ) + + known_invalid = ( + #bad char in otherwise correct hash + '$3$$7f8fe03093cc84b267b109625f6bbfxb', + ) + +#========================================================= #EOF #========================================================= diff --git a/passlib/utils/h64.py b/passlib/utils/h64.py index 78ac13c..90e14c2 100644 --- a/passlib/utils/h64.py +++ b/passlib/utils/h64.py @@ -10,15 +10,16 @@ import logging; log = logging.getLogger(__name__) __all__ = [ "CHARS", - "decode_6bit", "encode_6bit", - + "encode_bytes", "encode_3_offsets", "encode_2_offsets", "encode_1_offset", + "decode_int6", "encode_int6", "decode_int12", "encode_int12" "decode_int24", "encode_int24", "decode_int64", "encode_int64", + "decode_int", ] #================================================================================= @@ -92,17 +93,15 @@ def encode_bytes(source): # int <-> b64 string, used by des_crypt, ext_des_crypt #================================================================================= -def decode_int(value): - "decode hash-64 format used by crypt into integer" - #FORMAT: little-endian, each char contributes 6 bits, - # char value = index in H64_CHARS string - try: - out = 0 - for c in reversed(value): - out = (out<<6) + b64_decode_6bit(c) - return out - except KeyError: - raise ValueError, "invalid character in string" +def encode_int6(value): + "encode 6 bit integer to single char of hash-64 format" + return encode_6bit(value) + +def decode_int6(value): + "decode 1 char of hash-64 format, returning 6-bit integer" + return decode_6bit(value) + +#--------------------------------------------------------------------- def decode_int12(value): "decode 2 chars of hash-64 format used by crypt, returning 12-bit integer" @@ -115,6 +114,8 @@ def encode_int12(value): "encode 2 chars of hash-64 format from a 12-bit integer" return encode_6bit(value & 0x3f) + encode_6bit((value>>6) & 0x3f) +#--------------------------------------------------------------------- + def decode_int24(value): "decode 4 chars of hash-64 format, returning 24-bit integer" try: @@ -132,6 +133,8 @@ def encode_int24(value): encode_6bit((value>>12) & 0x3f) + \ encode_6bit((value>>18) & 0x3f) +#--------------------------------------------------------------------- + _RR9_1 = range(9,-1,-1) def decode_int64(value): @@ -147,6 +150,22 @@ def encode_int64(value): value >>= 6 return "".join(out) +#--------------------------------------------------------------------- + +def decode_int(value): + "decode hash-64 format used by crypt into integer" + #FORMAT: little-endian, each char contributes 6 bits, + # char value = index in H64_CHARS string + try: + out = 0 + for c in reversed(value): + out = (out<<6) + b64_decode_6bit(c) + return out + except KeyError: + raise ValueError, "invalid character in string" + +## def encode_int(value): + #================================================================================= #eof #================================================================================= diff --git a/passlib/win32.py b/passlib/win32.py index 9c90b59..1b55141 100644 --- a/passlib/win32.py +++ b/passlib/win32.py @@ -26,11 +26,12 @@ from binascii import hexlify #site #pkg from passlib.utils.des import des_encrypt_block -from passlib.utils.md4 import md4 +from passlib.hash import nthash +from passlib.hash.nthash import raw_nthash #local __all__ = [ - "lmhash", - "nthash", + "raw_lmhash", + "raw_nthash", ] #========================================================= #helpers @@ -44,11 +45,6 @@ def raw_lmhash(secret, hex=False): out = des_encrypt_block(ns[:7], LM_MAGIC) + des_encrypt_block(ns[7:], LM_MAGIC) return hexlify(out) if hex else out -def raw_nthash(secret, hex=False): - "encode password using md4-based NTHASH algorithm; returns string of raw bytes" - hash = md4(secret.encode("utf-16le")) - return hash.hexdigest() if hex else hash.digest() - #========================================================= #eoc #========================================================= |
