summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgignore2
-rw-r--r--docs/conf.py6
-rw-r--r--docs/lib/passlib.hash.md5_crypt.rst2
-rw-r--r--docs/lib/passlib.hash.nthash.rst35
-rw-r--r--docs/lib/passlib.hash.phpass.rst12
-rw-r--r--docs/lib/passlib.utils.h64.rst14
-rw-r--r--docs/lib/passlib.utils.rst2
-rw-r--r--passlib/hash/nthash.py99
-rw-r--r--passlib/hash/phpass.py2
-rw-r--r--passlib/tests/handler_utils.py6
-rw-r--r--passlib/tests/test_hash_misc.py (renamed from passlib/tests/test_hash_phpass.py)25
-rw-r--r--passlib/utils/h64.py45
-rw-r--r--passlib/win32.py12
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
#=========================================================