summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-02-01 17:33:50 +0000
committerEli Collins <elic@assurancetechnologies.com>2011-02-01 17:33:50 +0000
commitaa1b4e5f72fd6c73c4a1bce3de01d23e98f0e257 (patch)
treecdcda60503898319710e5140f55990afaab64fbb /passlib
parent36d54e254dd5f296be78e0958a4dbd3a04ee1e0b (diff)
downloadpasslib-aa1b4e5f72fd6c73c4a1bce3de01d23e98f0e257.tar.gz
added phpass portable hash
Diffstat (limited to 'passlib')
-rw-r--r--passlib/hash/md5_crypt.py2
-rw-r--r--passlib/hash/phpass.py154
-rw-r--r--passlib/tests/test_hash_phpass.py36
-rw-r--r--passlib/utils/h64.py18
4 files changed, 209 insertions, 1 deletions
diff --git a/passlib/hash/md5_crypt.py b/passlib/hash/md5_crypt.py
index 494e9d5..0c9a662 100644
--- a/passlib/hash/md5_crypt.py
+++ b/passlib/hash/md5_crypt.py
@@ -145,7 +145,7 @@ else:
#algorithm information
#=========================================================
name = "md5_crypt"
-#stats: 96 bit checksum, 48 bit salt
+#stats: 128 bit checksum, 48 bit salt
setting_kwds = ("salt",)
context_kwds = ()
diff --git a/passlib/hash/phpass.py b/passlib/hash/phpass.py
new file mode 100644
index 0000000..5be4708
--- /dev/null
+++ b/passlib/hash/phpass.py
@@ -0,0 +1,154 @@
+"""passlib.hash.phpass - PHPass Portable Crypt
+
+phppass located - http://www.openwall.com/phpass/
+algorithm described - http://www.openwall.com/articles/PHP-Users-Passwords
+
+phpass context - blowfish, ext_des_crypt, phpass
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from hashlib import md5
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_rounds, norm_salt, h64
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "phpass"
+#stats: 128 bit checksum, 24 bit salt
+
+setting_kwds = ("salt", "rounds")
+context_kwds = ()
+
+default_rounds = 9
+min_rounds = 7
+max_rounds = 30
+
+#=========================================================
+#internal helpers
+#=========================================================
+#$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0
+# $P$ 9 IQRaTwmf eRo7ud9Fh4E2PdI0S3r.L0
+
+_pat = re.compile(r"""
+ ^
+ \$
+ (?P<ident>[PH])
+ \$
+ (?P<rounds>[A-Za-z0-9./])
+ (?P<salt>[A-Za-z0-9./]{8})
+ (?P<chk>[A-Za-z0-9./]{22})?
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid phpass portable hash"
+ ident, rounds, salt, chk = m.group("ident", "rounds", "salt", "chk")
+ out = dict(
+ rounds=h64.decode_6bit(rounds),
+ salt=salt,
+ checksum=chk,
+ )
+ if ident != "P":
+ out['ident'] = ident
+ return out
+
+def render(rounds, salt, checksum=None, ident="P"):
+ if rounds < 0 or rounds > 31:
+ raise ValueError, "invalid rounds"
+ return "$%s$%s%s%s" % (ident, h64.encode_6bit(rounds), salt, checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None, ident="P"):
+ """generate md5-crypt configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be between 8 characters.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :param rounds:
+ optional rounds parameter.
+
+ like bcrypt's rounds value, phpass' rounds value is logarithmic,
+ each increase of +1 will double the actual number of rounds used.
+
+ :param ident:
+
+ phpBB3 uses ``H`` instead of ``P`` for it's identifier.
+ this may be set to generate phpBB3 compatible hashes.
+
+ :returns:
+ phpass configuration string.
+ """
+ if ident not in ("P", "H"):
+ raise ValueError, "invalid ident: %r" % (ident,)
+ salt = norm_salt(salt, 8, name=name)
+ if rounds is None:
+ rounds = default_rounds
+ if rounds < 7 or rounds > 30:
+ #NOTE: PHPass raises error when encrypting if rounds are outside these bounds.
+ raise ValueError, "rounds must be between 7..30 inclusive"
+ return render(rounds, salt, None, ident)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+ info = parse(config)
+ ident, rounds, salt = info.get("ident","P"), info['rounds'], info['salt']
+
+ #FIXME: can't find definitive policy on how phpass handles non-ascii.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ real_rounds = 1<<rounds
+ result = md5(salt + secret).digest()
+ r = 0
+ while r < real_rounds:
+ result = md5(result + secret).digest()
+ r += 1
+
+ checksum = h64.encode_bytes(result)
+ return render(rounds, salt, checksum, 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/tests/test_hash_phpass.py b/passlib/tests/test_hash_phpass.py
new file mode 100644
index 0000000..af90bd8
--- /dev/null
+++ b/passlib/tests/test_hash_phpass.py
@@ -0,0 +1,36 @@
+"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import hashlib
+from logging import getLogger
+#site
+#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
+#=========================================================
+class PHPassTest(_HandlerTestCase):
+ handler = mod
+
+ known_correct = (
+ ('', '$P$7JaFQsPzJSuenezefD/3jHgt5hVfNH0'),
+ ('compL3X!', '$P$FiS0N5L672xzQx1rt1vgdJQRYKnQM9/'),
+ ('test12345', '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0'), #from the source
+ )
+
+ known_invalid = (
+ #bad char in otherwise correct hash
+ '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r!L0',
+ )
+
+#=========================================================
+#EOF
+#=========================================================
diff --git a/passlib/utils/h64.py b/passlib/utils/h64.py
index 4d472c2..78ac13c 100644
--- a/passlib/utils/h64.py
+++ b/passlib/utils/h64.py
@@ -70,6 +70,24 @@ def encode_1_offset(buffer, o1):
v1 = ord(buffer[o1])
return encode_6bit(v1&0x3F) + encode_6bit(v1>>6)
+def encode_bytes(source):
+ "encode byte string to h64 format"
+ #FIXME: do something much more efficient here.
+ out = ''
+ end = len(source)
+ idx = 0
+ while idx <= end-3:
+ out += encode_3_offsets(source, idx, idx+1, idx+2)
+ idx += 3
+ if end % 3 == 1:
+ out += encode_1_offset(source, idx)
+ idx += 1
+ elif end % 3 == 2:
+ out += encode_2_offset(source, idx, idx+1)
+ idx += 2
+ assert idx == end
+ return out
+
#=================================================================================
# int <-> b64 string, used by des_crypt, ext_des_crypt
#=================================================================================