diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-02-01 17:33:50 +0000 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-02-01 17:33:50 +0000 |
| commit | aa1b4e5f72fd6c73c4a1bce3de01d23e98f0e257 (patch) | |
| tree | cdcda60503898319710e5140f55990afaab64fbb /passlib | |
| parent | 36d54e254dd5f296be78e0958a4dbd3a04ee1e0b (diff) | |
| download | passlib-aa1b4e5f72fd6c73c4a1bce3de01d23e98f0e257.tar.gz | |
added phpass portable hash
Diffstat (limited to 'passlib')
| -rw-r--r-- | passlib/hash/md5_crypt.py | 2 | ||||
| -rw-r--r-- | passlib/hash/phpass.py | 154 | ||||
| -rw-r--r-- | passlib/tests/test_hash_phpass.py | 36 | ||||
| -rw-r--r-- | passlib/utils/h64.py | 18 |
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 #================================================================================= |
