summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-02-14 13:40:41 -0500
committerEli Collins <elic@assurancetechnologies.com>2011-02-14 13:40:41 -0500
commit60557f422b8c4836fcd2f89ad9a21babca23e52f (patch)
treed28b5d97ed986251feedac89ce2ef86ee87bb032
parent43dfc4084ada0d073556b0aa26d67952f8a2a4e0 (diff)
downloadpasslib-60557f422b8c4836fcd2f89ad9a21babca23e52f.tar.gz
converted NTHash, PostgresMD5, SHA256Crypt, SunMD5Crypt to classes
-rw-r--r--passlib/hash/nthash.py134
-rw-r--r--passlib/hash/postgres_md5.py92
-rw-r--r--passlib/hash/sha1_crypt.py29
-rw-r--r--passlib/hash/sha256_crypt.py257
-rw-r--r--passlib/hash/sun_md5_crypt.py180
-rw-r--r--passlib/tests/handler_utils.py4
-rw-r--r--passlib/tests/test_hash_misc.py54
-rw-r--r--passlib/tests/test_hash_postgres.py14
-rw-r--r--passlib/tests/test_hash_sun_md5_crypt.py17
-rw-r--r--passlib/utils/handlers.py117
-rw-r--r--passlib/utils/pbkdf2.py21
11 files changed, 460 insertions, 459 deletions
diff --git a/passlib/hash/nthash.py b/passlib/hash/nthash.py
index 3dcd3dd..b92edd9 100644
--- a/passlib/hash/nthash.py
+++ b/passlib/hash/nthash.py
@@ -8,16 +8,14 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
+from passlib.base import register_crypt_handler
from passlib.utils.md4 import md4
from passlib.utils import autodocument
+from passlib.utils.handlers import BaseHandler
#pkg
#local
__all__ = [
- "genhash",
- "genconfig",
- "encrypt",
- "identify",
- "verify",
+ "NTHash",
]
#=========================================================
@@ -29,73 +27,87 @@ def raw_nthash(secret, hex=False):
return hash.hexdigest() if hex else hash.digest()
#=========================================================
-#algorithm information
+#handler
#=========================================================
-name = "nthash"
-#stats: 128 bit checksum, no salt
+class NTHash(BaseHandler):
+ #=========================================================
+ #class attrs
+ #=========================================================
+ name = "nthash"
+ setting_kwds = ("ident",)
-setting_kwds = ()
-context_kwds = ()
+ #=========================================================
+ #init
+ #=========================================================
+ _extra_init_settings = ("ident",)
-#=========================================================
-#internal helpers
-#=========================================================
-_pat = re.compile(r"""
- ^
- \$(?P<ident>3\$\$|NT\$)
- (?P<chk>[a-f0-9]{32})
- $
- """, re.X)
+ @classmethod
+ def norm_ident(cls, value, strict=False):
+ if value is None:
+ if strict:
+ raise ValueError, "no ident specified"
+ return "3"
+ if value not in ("3", "NT"):
+ raise ValueError, "invalid ident"
+ return value
-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
+ #=========================================================
+ #formatting
+ #=========================================================
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash) and (hash.startswith("$3$") or hash.startswith("$NT$"))
-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"
+ _pat = re.compile(r"""
+ ^
+ \$(?P<ident>3\$\$|NT\$)
+ (?P<chk>[a-f0-9]{32})
+ $
+ """, re.X)
-#=========================================================
-#primary interface
-#=========================================================
-def genconfig(ident=None):
- return render("0" * 32, ident)
+ @classmethod
+ def from_string(cls, hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = cls._pat.match(hash)
+ if not m:
+ raise ValueError, "invalid nthash"
+ ident, chk = m.group("ident", "chk")
+ return cls(ident=ident.strip("$"), checksum=chk, strict=True)
-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'))
+ def to_string(self):
+ ident = self.ident
+ if ident == "3":
+ return "$3$$" + self.checksum
+ else:
+ assert ident == "NT"
+ return "$NT$" + self.checksum
-#=========================================================
-#secondary interface
-#=========================================================
-def encrypt(secret, **settings):
- return genhash(secret, genconfig(**settings))
+ #=========================================================
+ #primary interface
+ #=========================================================
+ _stub_checksum = "0" * 32
+
+ @classmethod
+ def genconfig(cls, ident=None):
+ return cls(ident=ident, checksum=self._stub_checksum).to_string()
-def verify(secret, hash):
- return hash == genhash(secret, hash)
+ def calc_checksum(self, secret):
+ if secret is None:
+ raise TypeError, "secret must be a string"
+ return raw_nthash(secret, hex=True)
-def identify(hash):
- return bool(hash and _pat.match(hash))
+ #=========================================================
+ #eoc
+ #=========================================================
-autodocument(globals())
+autodocument(NTHash, settings_doc="""
+:param ident:
+ This handler supports two different :ref:`modular-crypt-format` identifiers.
+ It defaults to ``3``, but users may specify the alternate ``NT`` identifier
+ which is used in some contexts.
+""")
+register_crypt_handler(NTHash)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/postgres_md5.py b/passlib/hash/postgres_md5.py
index 64c92c3..cafb9b1 100644
--- a/passlib/hash/postgres_md5.py
+++ b/passlib/hash/postgres_md5.py
@@ -10,64 +10,74 @@ from warnings import warn
#site
#libs
#pkg
+from passlib.base import register_crypt_handler
from passlib.utils import autodocument
#local
__all__ = [
- "genhash",
- "genconfig",
- "encrypt",
- "identify",
- "verify",
+ "PostgresMD5",
]
#=========================================================
-#backend
+#handler
#=========================================================
+class PostgresMD5(object):
+ #=========================================================
+ #algorithm information
+ #=========================================================
+ name = "postgres_md5"
+ setting_kwds = ()
+ context_kwds = ("user",)
-#=========================================================
-#algorithm information
-#=========================================================
-name = "postgres_md5"
-#stats: 512 bit checksum, username used as salt
+ #=========================================================
+ #formatting
+ #=========================================================
+ _pat = re.compile(r"^md5[0-9a-f]{32}$")
-setting_kwds = ()
-context_kwds = ("user",)
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash and cls._pat.match(hash))
-#=========================================================
-#internal helpers
-#=========================================================
-_pat = re.compile(r"^md5[0-9a-f]{32}$")
+ #=========================================================
+ #primary interface
+ #=========================================================
+ @classmethod
+ def genconfig(cls):
+ return None
-#=========================================================
-#primary interface
-#=========================================================
-def genconfig():
- return None
-
-def genhash(secret, config, user):
- if config and not identify(config):
- raise ValueError, "not a postgres-md5 hash"
- if not user:
- raise ValueError, "user keyword must be specified for this algorithm"
- return "md5" + md5(secret + user).hexdigest().lower()
+ @classmethod
+ def genhash(cls, secret, config, user):
+ if config and not cls.identify(config):
+ raise ValueError, "not a postgres-md5 hash"
+ return cls.encrypt(secret, user)
-#=========================================================
-#secondary interface
-#=========================================================
-def encrypt(secret, user, **settings):
- return genhash(secret, genconfig(**settings), user)
+ #=========================================================
+ #secondary interface
+ #=========================================================
+ @classmethod
+ def encrypt(cls, secret, user):
+ #FIXME: not sure what postgres' policy is for unicode
+ if not user:
+ raise ValueError, "user keyword must be specified for this algorithm"
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ if isinstance(user, unicode):
+ user = user.encode("utf-8")
+ return "md5" + md5(secret + user).hexdigest().lower()
-def verify(secret, hash, user):
- if not hash:
- raise ValueError, "no hash specified"
- return hash.lower() == genhash(secret, hash, user)
+ @classmethod
+ def verify(cls, secret, hash, user):
+ if not hash:
+ raise ValueError, "no hash specified"
+ return hash == cls.genhash(secret, hash, user)
-def identify(hash):
- return bool(hash and _pat.match(hash))
+ #=========================================================
+ #eoc
+ #=========================================================
-autodocument(globals(), context_doc="""\
+autodocument(PostgresMD5, context_doc="""\
:param user: string containing name of postgres user account this password is associated with.
""")
+register_crypt_handler(PostgresMD5)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/sha1_crypt.py b/passlib/hash/sha1_crypt.py
index 3ba7f24..39b5dd5 100644
--- a/passlib/hash/sha1_crypt.py
+++ b/passlib/hash/sha1_crypt.py
@@ -12,40 +12,19 @@ import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
-try:
- from M2Crypto import EVP as _EVP
-except ImportError:
- _EVP = None
#libs
from passlib.utils import norm_rounds, norm_salt, autodocument, h64
from passlib.utils.handlers import BaseHandler
+from passlib.utils.pbkdf2 import hmac_sha1
from passlib.base import register_crypt_handler
#pkg
#local
__all__ = [
]
-
-#=========================================================
-#backend
-#=========================================================
-def hmac_sha1(key, msg):
- return hmac(key, msg, sha1).digest()
-
-if _EVP:
- try:
- result = _EVP.hmac('x','y') #default *should* be sha1, which saves us a wrapper, but might as well check.
- except ValueError:
- pass
- else:
- if result == ',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i':
- hmac_sha1 = _EVP.hmac
-
-#TODO: should test for crypt support (NetBSD only)
-
#=========================================================
#sha1-crypt
#=========================================================
-class Sha1Crypt(BaseHandler):
+class SHA1Crypt(BaseHandler):
#=========================================================
#class attrs
@@ -129,8 +108,8 @@ class Sha1Crypt(BaseHandler):
#eoc
#=========================================================
-autodocument(Sha1Crypt)
-register_crypt_handler(Sha1Crypt)
+autodocument(SHA1Crypt)
+register_crypt_handler(SHA1Crypt)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/sha256_crypt.py b/passlib/hash/sha256_crypt.py
index a7cd1fd..8b53345 100644
--- a/passlib/hash/sha256_crypt.py
+++ b/passlib/hash/sha256_crypt.py
@@ -9,15 +9,13 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import norm_rounds, norm_salt, h64, autodocument
+from passlib.base import register_crypt_handler
+from passlib.utils import h64, autodocument, os_crypt
+from passlib.utils.handlers import BackendBaseHandler
#pkg
#local
__all__ = [
- "genhash",
- "genconfig",
- "encrypt",
- "identify",
- "verify",
+ "SHA256Crypt",
]
#=========================================================
@@ -170,145 +168,130 @@ _256_offsets = (
)
#=========================================================
-#choose backend
+#handler
#=========================================================
+class SHA256Crypt(BackendBaseHandler):
-#fallback to default backend (defined above)
-backend = "builtin"
+ #=========================================================
+ #algorithm information
+ #=========================================================
+ name = "sha256_crypt"
-#check if stdlib crypt is available, and if so, if OS supports $5$ and $6$
-#XXX: is this test expensive enough it should be delayed
-#until sha-crypt is requested?
+ setting_kwds = ("salt", "rounds", "implicit_rounds")
-try:
- from crypt import crypt
-except ImportError:
- crypt = None
-else:
- if crypt("test", "$5$rounds=1000$test") == "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/":
- backend = "os-crypt"
- else:
- crypt = None
-
-#=========================================================
-#algorithm information
-#=========================================================
-name = "sha256_crypt"
-#stats: 256 bit checksum, 96 bit salt, 1000..10e8-1 rounds
-
-setting_kwds = ("salt", "rounds")
-context_kwds = ()
-
-default_rounds = 40000 #current passlib default
-min_rounds = 1000
-max_rounds = 999999999
-rounds_cost = "linear"
-
-min_salt_chars = 0
-max_salt_chars = 16
+ min_salt_chars = 0
+ max_salt_chars = 16
+ #TODO: allow salt charset 0-255 except for "\x00\n:$"
-#=========================================================
-#internal helpers
-#=========================================================
-_pat = re.compile(r"""
- ^
- \$5
- (\$rounds=(?P<rounds>\d+))?
- \$
- (
- (?P<salt1>[^:$]*)
- |
- (?P<salt2>[^:$]{0,16})
+ default_rounds = 40000 #current passlib default
+ min_rounds = 1000
+ max_rounds = 999999999
+ rounds_cost = "linear"
+
+ #=========================================================
+ #init
+ #=========================================================
+ def __init__(self, implicit_rounds=None, **kwds):
+ if implicit_rounds is None:
+ implicit_rounds = True
+ self.implicit_rounds = implicit_rounds
+ super(SHA512Crypt, self).__init__(**kwds)
+
+ #=========================================================
+ #parsing
+ #=========================================================
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash) and hash.startswith("$5$")
+
+ #: regexp used to parse hashes
+ _pat = re.compile(r"""
+ ^
+ \$5
+ (\$rounds=(?P<rounds>\d+))?
\$
- (?P<chk>[A-Za-z0-9./]{43})?
- )
- $
- """, re.X)
-
-def parse(hash):
- if not hash:
- raise ValueError, "no hash specified"
- m = _pat.match(hash)
- if not m:
- raise ValueError, "invalid sha256-crypt hash"
- rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk")
- if rounds and rounds.startswith("0"):
- raise ValueError, "invalid sha256-crypt hash: zero-padded rounds"
- return dict(
- implicit_rounds = not rounds,
- rounds=int(rounds) if rounds else 5000,
- salt=salt1 or salt2,
- checksum=chk,
- )
-
-def render(rounds, salt, checksum=None, implicit_rounds=True):
- assert '$' not in salt
- if rounds == 5000 and implicit_rounds:
- return "$5$%s$%s" % (salt, checksum or '')
- else:
- return "$5$rounds=%d$%s$%s" % (rounds, salt, checksum or '')
-
-#=========================================================
-#primary interface
-#=========================================================
-def genconfig(salt=None, rounds=None, implicit_rounds=True):
- """generate sha256-crypt configuration string
-
- :param salt:
- optional salt string to use.
-
- if omitted, one will be automatically generated (recommended).
-
- length must be 0 .. 16 characters inclusive.
- characters must be in range ``A-Za-z0-9./``.
-
- :param rounds:
-
- optional number of rounds, must be between 1000 and 999999999 inclusive.
-
- :param implicit_rounds:
-
- this is an internal option which generally doesn't need to be touched.
-
- :returns:
- sha256-crypt configuration string.
- """
- #TODO: allow salt charset 0-255 except for "\x00\n:$"
- salt = norm_salt(salt, min_salt_chars, max_salt_chars, name=name)
- rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
- return render(rounds, salt, None, implicit_rounds)
-
-def genhash(secret, config):
- #parse and run through genconfig to validate configuration
- info = parse(config)
- info.pop("checksum")
- config = genconfig(**info)
-
- #run through chosen backend
- if crypt:
- #using system's crypt routine.
+ (
+ (?P<salt1>[^:$]*)
+ |
+ (?P<salt2>[^:$]{0,16})
+ \$
+ (?P<chk>[A-Za-z0-9./]{43})?
+ )
+ $
+ """, re.X)
+
+ @classmethod
+ def from_string(cls, hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ #TODO: write non-regexp based parser,
+ # and rely on norm_salt etc to handle more of the validation.
+ m = cls._pat.match(hash)
+ if not m:
+ raise ValueError, "invalid sha256-crypt hash"
+ rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk")
+ if rounds and rounds.startswith("0"):
+ raise ValueError, "invalid sha256-crypt hash (zero-padded rounds)"
+ return cls(
+ implicit_rounds = not rounds,
+ rounds=int(rounds) if rounds else 5000,
+ salt=salt1 or salt2,
+ checksum=chk,
+ strict=bool(chk),
+ )
+
+ def to_string(self):
+ if self.rounds == 5000 and self.implicit_rounds:
+ return "$5$%s$%s" % (self.salt, self.checksum or '')
+ else:
+ return "$5$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '')
+
+ #=========================================================
+ #backend
+ #=========================================================
+ backends = ("os_crypt", "builtin")
+
+ _has_backend_builtin = True
+
+ @classproperty
+ def _has_backend_os_crypt(cls):
+ return bool(
+ os_crypt and
+ os_crypt("test", "$5$rounds=1000$test") ==
+ "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/"
+ )
+
+ def _calc_checksum_builtin(self, secret):
+ checksum, salt, rounds = raw_sha256_crypt(secret, self.salt, self.rounds)
+ assert salt == self.salt, "class doesn't agree w/ builtin backend"
+ assert rounds == self.rounds, "class doesn't agree w/ builtin backend"
+ return checksum
+
+ def _calc_checksum_os_crypt(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- return crypt(secret, config)
- else:
- #using builtin routine
- info = parse(config)
- checksum, salt, rounds = raw_sha256_crypt(secret, info['salt'], info['rounds'])
- return render(rounds, salt, checksum, info['implicit_rounds'])
-
-#=========================================================
-#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))
-
-autodocument(globals())
+ #NOTE: avoiding full parsing routine via from_string().checksum,
+ # and just extracting the bit we need.
+ result = os_crypt(secret, self.to_string())
+ assert result.startswith("$5$")
+ chk = result[-43:]
+ assert '$' not in chk
+ return chk
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+autodocument(SHA256Crypt, settings_doc="""
+:param implicit_rounds:
+ this is an internal option which generally doesn't need to be touched.
+
+ this flag determines whether the hash should omit the rounds parameter
+ when encoding it to a string; this is only permitted by the spec for rounds=5000,
+ and the flag is ignored otherwise. the spec requires the two different
+ encodings be preserved as they are, instead of normalizing them.
+""")
+register_crypt_handler(SHA256Crypt)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/sun_md5_crypt.py b/passlib/hash/sun_md5_crypt.py
index 352a344..8f30a98 100644
--- a/passlib/hash/sun_md5_crypt.py
+++ b/passlib/hash/sun_md5_crypt.py
@@ -25,7 +25,9 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import norm_rounds, norm_salt, h64, autodocument
+from passlib.base import register_crypt_handler
+from passlib.utils import h64, autodocument
+from passlib.utils.handlers import BaseHandler
#pkg
#local
__all__ = [
@@ -186,101 +188,89 @@ _chk_offsets = (
)
#=========================================================
-#algorithm information
+#handler
#=========================================================
-name = "sun_md5_crypt"
-#stats: 128 bit checksum, 48 bit salt, 0..2**32-4095 rounds
-
-setting_kwds = ("salt", "rounds")
-context_kwds = ()
-
-min_salt_chars = 0
-max_salt_chars = 8
-
-default_rounds = 5000 #current passlib default
-min_rounds = 0
-max_rounds = 4294963199 ##2**32-1-4096
- #XXX: not sure what it does if past this bound... does 32 int roll over?
-rounds_cost = "linear"
-
-#=========================================================
-#internal helpers
-#=========================================================
-_pat = re.compile(r"""
- ^
- \$md5
- ([$,]rounds=(?P<rounds>\d+))?
- \$(?P<salt>[A-Za-z0-9./]{0,8})
- (\$(?P<chk>[A-Za-z0-9./]{22})?)?
- $
- """, re.X)
-
-#NOTE: trailing "$" is supposed to be part of config string,
-# supposed to take both, but render with "$"
-#NOTE: seen examples with both "," or "$" as md5/rounds separator,
-# not sure what official format is.
-# taking both, rendering ","
-
-def parse(hash):
- if not hash:
- raise ValueError, "no hash specified"
- m = _pat.match(hash)
- if not m:
- raise ValueError, "invalid sun-md5-crypt hash"
- rounds, salt, chk = m.group("rounds", "salt", "chk")
- #NOTE: this is *additional* rounds added to base 4096 specified by spec.
- #XXX: should we note whether "$" or "," was used as rounds separator?
- # not sure if that affects anything
- return dict(
- rounds=int(rounds) if rounds else 0,
- salt=salt,
- checksum=chk,
- )
-
-def render(rounds, salt, checksum=None):
- "render a sun-md5-crypt hash or config string"
- if rounds > 0:
- return "$md5,rounds=%d$%s$%s" % (rounds, salt, checksum or '')
- else:
- return "$md5$%s$%s" % (salt, checksum or '')
-
-#=========================================================
-#primary interface
-#=========================================================
-def genconfig(salt=None, rounds=None):
- salt = norm_salt(salt, min_salt_chars, max_salt_chars, name=name)
- rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
- return render(rounds, salt, None)
-
-def genhash(secret, config):
- #parse and run through genconfig to validate configuration
- #FIXME: could eliminate uneeded render/parse call
- info = parse(config)
- info.pop("checksum")
- config = genconfig(**info)
- info = parse(config)
- rounds, salt = info['rounds'], info['salt']
-
- #run through builtin backend
- checksum = raw_sun_md5_crypt(secret, rounds, salt)
- return render(rounds, salt, checksum)
-
-#=========================================================
-#secondary interface
-#=========================================================
-def encrypt(secret, **settings):
- return genhash(secret, genconfig(**settings))
-
-def verify(secret, hash):
- #normalize hash format so strings compare
- if hash and hash.startswith("$md5$rounds="):
- hash = "$md5,rounds=" + hash[12:]
- return hash == genhash(secret, hash)
-
-def identify(hash):
- return bool(hash and _pat.match(hash))
-
-autodocument(globals())
+class SunMD5Crypt(BaseHandler):
+ #=========================================================
+ #class attrs
+ #=========================================================
+ name = "sun_md5_crypt"
+ setting_kwds = ("salt", "rounds")
+
+ min_salt_chars = 0
+ max_salt_chars = 8
+
+ default_rounds = 5000 #current passlib default
+ min_rounds = 0
+ max_rounds = 4294963199 ##2**32-1-4096
+ #XXX: ^ not sure what it does if past this bound... does 32 int roll over?
+ rounds_cost = "linear"
+
+ #=========================================================
+ #internal helpers
+ #=========================================================
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash) and (hash.startswith("$md5$") or hash.startswith("$md5,"))
+
+ _pat = re.compile(r"""
+ ^
+ \$md5
+ ([$,]rounds=(?P<rounds>\d+))?
+ \$(?P<salt>[A-Za-z0-9./]{0,8})
+ (\$(?P<chk>[A-Za-z0-9./]{22})?)?
+ $
+ """, re.X)
+
+ #NOTE: trailing "$" is supposed to be part of config string,
+ # supposed to take both, but render with "$"
+ #NOTE: seen examples with both "," or "$" as md5/rounds separator,
+ # not sure what official format is.
+ # taking both, rendering ","
+
+ @classmethod
+ def from_string(cls, hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = cls._pat.match(hash)
+ if not m:
+ raise ValueError, "invalid sun-md5-crypt hash"
+ rounds, salt, chk = m.group("rounds", "salt", "chk")
+ #NOTE: this is *additional* rounds added to base 4096 specified by spec.
+ #XXX: should we note whether "$" or "," was used as rounds separator?
+ # not sure if that affects anything
+ return cls(
+ rounds=int(rounds) if rounds else 0,
+ salt=salt,
+ checksum=chk,
+ strict=bool(chk)
+ )
+
+ def to_string(self):
+ rounds = self.rounds
+ if rounds > 0:
+ out = "$md5,rounds=%d$%s" % (rounds, self.salt)
+ else:
+ out = "$md5$%s" % (self.salt,)
+ chk = self.checksum
+ if chk:
+ out = "%s$%s" % (out, chk)
+ return out
+
+ #=========================================================
+ #primary interface
+ #=========================================================
+ #TODO: if we're on solaris, check for native crypt() support
+
+ def calc_checksum(self, secret):
+ return raw_sun_md5_crypt(secret, self.rounds, self.salt)
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+autodocument(SunMD5Crypt)
+register_crypt_handler(SunMD5Crypt)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/tests/handler_utils.py b/passlib/tests/handler_utils.py
index 0ecdff3..988eddd 100644
--- a/passlib/tests/handler_utils.py
+++ b/passlib/tests/handler_utils.py
@@ -33,8 +33,8 @@ class _HandlerTestCase(TestCase):
#specify handler object here
handler = None
- #NOTE: would like unicode support for all hashes. until then, this flag is set for those which aren't.
- supports_unicode = False
+ #this option is available for hashes which can't handle unicode
+ supports_unicode = True
#maximum number of chars which hash will include in checksum
#override this only if hash doesn't use all chars (the default)
diff --git a/passlib/tests/test_hash_misc.py b/passlib/tests/test_hash_misc.py
index f0742c6..a684bcb 100644
--- a/passlib/tests/test_hash_misc.py
+++ b/passlib/tests/test_hash_misc.py
@@ -14,6 +14,24 @@ from passlib.tests.utils import enable_option
log = getLogger(__name__)
#=========================================================
+#NTHASH for unix
+#=========================================================
+from passlib.hash.nthash import NTHash
+
+class NTHashTest(_HandlerTestCase):
+ handler = NTHash
+
+ known_correct = (
+ ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'),
+ ('passphrase', '$NT$7f8fe03093cc84b267b109625f6bbf4b'),
+ )
+
+ known_identified_invalid = [
+ #bad char in otherwise correct hash
+ '$3$$7f8fe03093cc84b267b109625f6bbfxb',
+ ]
+
+#=========================================================
#PHPass Portable Crypt
#=========================================================
from passlib.hash import phpass
@@ -33,24 +51,6 @@ 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',
- )
-
-#=========================================================
# netbsd sha1 crypt
#=========================================================
from passlib.hash import sha1_crypt
@@ -69,5 +69,23 @@ class SHA1CryptTest(_HandlerTestCase):
]
#=========================================================
+#sun md5 crypt
+#=========================================================
+from passlib.hash.sun_md5_crypt import SunMD5Crypt
+
+class SunMD5CryptTest(_HandlerTestCase):
+ handler = SunMD5Crypt
+
+ known_correct = [
+ #sample hash found at http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9
+ ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"),
+ ]
+
+ known_identified_invalid = [
+ #bad char in otherwise correct hash
+ "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/"
+ ]
+
+#=========================================================
#EOF
#=========================================================
diff --git a/passlib/tests/test_hash_postgres.py b/passlib/tests/test_hash_postgres.py
index ebef4e9..a47af4d 100644
--- a/passlib/tests/test_hash_postgres.py
+++ b/passlib/tests/test_hash_postgres.py
@@ -9,25 +9,25 @@ from logging import getLogger
#site
#pkg
from passlib.tests.handler_utils import _HandlerTestCase
-import passlib.hash.postgres_md5 as mod
+from passlib.hash.postgres_md5 import PostgresMD5
#module
log = getLogger(__name__)
#=========================================================
#database hashes
#=========================================================
-class PostgresMd5CryptTest(_HandlerTestCase):
- handler = mod
- known_correct = (
+class PostgresMD5CryptTest(_HandlerTestCase):
+ handler = PostgresMD5
+ known_correct = [
# ((secret,user),hash)
(('mypass', 'postgres'), 'md55fba2ea04fd36069d2574ea71c8efe9d'),
(('mypass', 'root'), 'md540c31989b20437833f697e485811254b'),
(("testpassword",'testuser'), 'md5d4fc5129cc2c25465a5370113ae9835f'),
- )
- known_invalid = (
+ ]
+ known_invalid = [
#bad 'z' char in otherwise correct hash
'md54zc31989b20437833f697e485811254b',
- )
+ ]
#NOTE: used to support secret=(password, user) format, but removed it for now.
##def test_tuple_mode(self):
diff --git a/passlib/tests/test_hash_sun_md5_crypt.py b/passlib/tests/test_hash_sun_md5_crypt.py
index 56a6e11..964974a 100644
--- a/passlib/tests/test_hash_sun_md5_crypt.py
+++ b/passlib/tests/test_hash_sun_md5_crypt.py
@@ -9,26 +9,9 @@ from logging import getLogger
#site
#pkg
from passlib.tests.handler_utils import _HandlerTestCase
-import passlib.hash.sun_md5_crypt as mod
#module
log = getLogger(__name__)
#=========================================================
-#hash alg
-#=========================================================
-class SunMd5CryptTest(_HandlerTestCase):
- handler = mod
-
- known_correct = [
- #sample hash found at http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9
- ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"),
- ]
-
- known_invalid = (
- #bad char in otherwise correct hash
- "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/"
- )
-
-#=========================================================
#EOF
#=========================================================
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index 74c2f7b..4ae8841 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -194,7 +194,6 @@ class BaseHandler(object):
default_rounds = None #if not specified, BaseHandler.norm_rounds() will require explicit rounds value every time
rounds_cost = "linear" #common case
-
#----------------------------------------------
#misc BaseHandler configuration
#----------------------------------------------
@@ -212,6 +211,7 @@ class BaseHandler(object):
#init
#=========================================================
#XXX: rename strict kwd to _strict ?
+ #XXX: for from_string() purposes, a strict_salt kwd to override strict, might also be useful
def __init__(self, checksum=None, salt=None, rounds=None, strict=False, **kwds):
self.checksum = self.norm_checksum(checksum, strict=strict)
self.salt = self.norm_salt(salt, strict=strict)
@@ -268,8 +268,31 @@ class BaseHandler(object):
raise AssertionError, "unknown rounds cost function"
#=========================================================
- #helpers
+ #init helpers
#=========================================================
+
+ #---------------------------------------------------------
+ #internal tests for features
+ #---------------------------------------------------------
+ @classproperty
+ def _has_salt(cls):
+ "attr for checking if salts are supported, optimizes itself on first use"
+ if cls is BaseHandler:
+ raise RuntimeError, "not allowed for BaseHandler directly"
+ value = cls._has_salt = 'salt' in cls.setting_kwds
+ return value
+
+ @classproperty
+ def _has_rounds(cls):
+ "attr for checking if variable are supported, optimizes itself on first use"
+ if cls is BaseHandler:
+ raise RuntimeError, "not allowed for BaseHandler directly"
+ value = cls._has_rounds = 'rounds' in cls.setting_kwds
+ return value
+
+ #---------------------------------------------------------
+ #normalization/validation helpers
+ #---------------------------------------------------------
@classmethod
def norm_checksum(cls, checksum, strict=False):
if checksum is None:
@@ -282,14 +305,6 @@ class BaseHandler(object):
raise ValueError, "invalid characters in %s checksum" % (cls.name,)
return checksum
- @classproperty
- def _has_salt(cls):
- "attr for checking if salts are supported, optimizes itself on first use"
- if cls is BaseHandler:
- raise RuntimeError, "not allowed for BaseHandler directly"
- value = cls._has_salt = 'salt' in cls.setting_kwds
- return value
-
@classmethod
def norm_salt(cls, salt, strict=False):
"helper to normalize salt string; strict flag causes error even for correctable errors"
@@ -317,14 +332,6 @@ class BaseHandler(object):
return salt
- @classproperty
- def _has_rounds(cls):
- "attr for checking if variable are supported, optimizes itself on first use"
- if cls is BaseHandler:
- raise RuntimeError, "not allowed for BaseHandler directly"
- value = cls._has_rounds = 'rounds' in cls.setting_kwds
- return value
-
@classmethod
def norm_rounds(cls, rounds, strict=False):
"helper to normalize rounds value; strict flag causes error even for correctable errors"
@@ -359,6 +366,42 @@ class BaseHandler(object):
return rounds
#=========================================================
+ #password hash api - formatting interface
+ #=========================================================
+ @classmethod
+ def identify(cls, hash):
+ #NOTE: subclasses may wish to use faster / simpler identify,
+ # and raise value errors only when an invalid (but identifiable) string is parsed
+ if not hash:
+ return False
+ try:
+ cls.from_string(hash)
+ return True
+ except ValueError:
+ return False
+
+ @classmethod
+ def from_string(cls, hash):
+ "return parsed instance from hash/configuration string; raising ValueError on invalid inputs"
+ raise NotImplementedError, "%s must implement from_string()" % (cls,)
+
+ def to_string(self):
+ "render instance to hash or configuration string (depending on if checksum attr is set)"
+ raise NotImplementedError, "%s must implement from_string()" % (type(self),)
+
+ ##def to_config_string(self):
+ ## "helper for generating configuration string (ignoring hash)"
+ ## chk = self.checksum
+ ## if chk:
+ ## try:
+ ## self.checksum = None
+ ## return self.to_string()
+ ## finally:
+ ## self.checksum = chk
+ ## else:
+ ## return self.to_string()
+
+ #=========================================================
#password hash api - primary interface (default implementation)
#=========================================================
@classmethod
@@ -379,18 +422,6 @@ class BaseHandler(object):
#password hash api - secondary interface (default implementation)
#=========================================================
@classmethod
- def identify(cls, hash):
- #NOTE: subclasses may wish to use faster / simpler identify,
- # and raise value errors only when an invalid (but identifiable) string is parsed
- if not hash:
- return False
- try:
- cls.from_string(hash)
- return True
- except ValueError:
- return False
-
- @classmethod
def encrypt(cls, secret, **settings):
self = cls(**settings)
self.checksum = self.calc_checksum(secret)
@@ -405,31 +436,7 @@ class BaseHandler(object):
return self.checksum == self.calc_checksum(secret)
#=========================================================
- #password hash api - parsing interface
- #=========================================================
- @classmethod
- def from_string(cls, hash):
- "return parsed instance from hash/configuration string; raising ValueError on invalid inputs"
- raise NotImplementedError, "%s must implement from_string()" % (cls,)
-
- def to_string(self):
- "render instance to hash or configuration string (depending on if checksum attr is set)"
- raise NotImplementedError, "%s must implement from_string()" % (type(self),)
-
- def to_config_string(self):
- "helper for generating configuration string (ignoring hash)"
- chk = self.checksum
- if chk:
- try:
- self.checksum = None
- return self.to_string()
- finally:
- self.checksum = chk
- else:
- return self.to_string()
-
- #=========================================================
- #
+ #eoc
#=========================================================
#=========================================================
diff --git a/passlib/utils/pbkdf2.py b/passlib/utils/pbkdf2.py
index 0c62ada..89f0f0e 100644
--- a/passlib/utils/pbkdf2.py
+++ b/passlib/utils/pbkdf2.py
@@ -12,6 +12,7 @@ import hmac
import logging; log = logging.getLogger(__name__)
import re
from struct import pack
+from warnings import warn
#site
try:
from M2Crypto import EVP as _EVP
@@ -21,10 +22,29 @@ except ImportError:
from passlib.utils import xor_bytes
#local
__all__ = [
+ "hmac_sha1",
"pbkdf2",
]
#=================================================================================
+#hmac sha1 support
+#=================================================================================
+def hmac_sha1(key, msg):
+ "perform raw hmac-sha1 of a message"
+ return hmac(key, msg, sha1).digest()
+
+if _EVP:
+ #default *should* be sha1, which saves us a wrapper function, but might as well check.
+ try:
+ result = _EVP.hmac('x','y')
+ except ValueError:
+ #this is probably not a good sign if it happens.
+ warn("PassLib: M2Crypt.EVP.hmac() unexpected threw value error during passlib startup test")
+ else:
+ if result == ',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i':
+ hmac_sha1 = _EVP.hmac
+
+#=================================================================================
#backend
#=================================================================================
MAX_BLOCKS = 0xffffffffL #2**32-1
@@ -32,7 +52,6 @@ MAX_BLOCKS = 0xffffffffL #2**32-1
def _resolve_prf(prf):
"resolve prf string or callable -> func & digest_size"
if isinstance(prf, str):
-
if prf.startswith("hmac-"):
digest = prf[5:]