summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/lib/passlib.hash.ext_des_crypt.rst1
-rw-r--r--passlib/hash/des_crypt.py170
-rw-r--r--passlib/hash/ext_des_crypt.py149
-rw-r--r--passlib/hash/mysql_323.py119
-rw-r--r--passlib/hash/phpass.py234
-rw-r--r--passlib/hash/sha1_crypt.py41
-rw-r--r--passlib/hash/sha512_crypt.py115
-rw-r--r--passlib/tests/test_hash_des_crypt.py8
-rw-r--r--passlib/tests/test_hash_misc.py12
-rw-r--r--passlib/tests/test_hash_mysql.py10
-rw-r--r--passlib/tests/test_hash_sha_crypt.py37
-rw-r--r--passlib/utils/__init__.py9
-rw-r--r--passlib/utils/handlers.py759
13 files changed, 797 insertions, 867 deletions
diff --git a/docs/lib/passlib.hash.ext_des_crypt.rst b/docs/lib/passlib.hash.ext_des_crypt.rst
index 7bc676d..d0c2676 100644
--- a/docs/lib/passlib.hash.ext_des_crypt.rst
+++ b/docs/lib/passlib.hash.ext_des_crypt.rst
@@ -75,3 +75,4 @@ This implementation of ext-des-crypt differs from others in one way:
References
==========
* `<http://fuse4bsd.creo.hu/localcgi/man-cgi.cgi?crypt+3>`_ - primary source used for description of ext-des-crypt format & algorithm
+* `<http://ftp.lava.net/cgi-bin/bsdi-man?proto=1.1&query=crypt&msection=3&apropos=0>`_ - another source describing algorithm
diff --git a/passlib/hash/des_crypt.py b/passlib/hash/des_crypt.py
index e724909..5cad5b8 100644
--- a/passlib/hash/des_crypt.py
+++ b/passlib/hash/des_crypt.py
@@ -9,7 +9,9 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
+from passlib.base import register_crypt_handler
from passlib.utils import norm_salt, h64, autodocument
+from passlib.utils.handlers import BaseHandler
from passlib.utils.des import mdes_encrypt_int_block
#pkg
#local
@@ -78,98 +80,82 @@ except ImportError:
crypt = None
#=========================================================
-#algorithm information
-#=========================================================
-name = "des_crypt"
-#stats: 64 bit checksum, 12 bit salt, max 8 chars of secret
-
-setting_kwds = ("salt",)
-context_kwds = ()
-
-min_salt_chars = max_salt_chars = 2
-
-#=========================================================
-#internal helpers
-#=========================================================
-#FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
-_pat = re.compile(r"""
- ^
- (?P<salt>[./a-z0-9]{2})
- (?P<chk>[./a-z0-9]{11})?
- $""", re.X|re.I)
-
-def parse(hash):
- if not hash:
- raise ValueError, "no hash specified"
- m = _pat.match(hash)
- if not m:
- raise ValueError, "invalid des-crypt hash"
- salt, chk = m.group("salt", "chk")
- return dict(
- salt=salt,
- checksum=chk,
- )
-
-def render(salt, checksum=None):
- if len(salt) < 2:
- raise ValueError, "invalid salt"
- return "%s%s" % (salt[:2], checksum or '')
-
-#=========================================================
-#primary interface
-#=========================================================
-def genconfig(salt=None):
- salt = norm_salt(salt, min_salt_chars, max_salt_chars, name=name)
- return render(salt, None)
-
-def genhash(secret, config):
- #parse and run through genconfig to validate configuration
- info = parse(config)
- info.pop("checksum")
- config = genconfig(**info)
-
- #forbidding nul chars because linux crypt (and most C implementations) won't accept it either.
- if '\x00' in secret:
- raise ValueError, "null char in secret"
-
- #XXX: des-crypt predates unicode, not sure if there's an official policy for handing it.
- #for now, just coercing to utf-8.
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
-
- #run through chosen backend
- if crypt:
- #XXX: given a single letter salt, linux crypt returns a hash with the original salt doubled,
- # but appears to calculate the hash based on the letter + "G" as the second byte.
- # this results in a hash that won't validate, which is DEFINITELY wrong.
- # need to find out it's underlying logic, and if it's part of spec,
- # or just weirdness that should actually be an error.
- # until then, passlib raises an error in genconfig()
-
- #XXX: given salt chars outside of h64.CHARS range, linux crypt
- # does something unknown when decoding salt to 12 bit int,
- # successfully creates a hash, but reports the original salt.
- # need to find out it's underlying logic, and if it's part of spec,
- # or just weirdness that should actually be an error.
- # until then, passlib raises an error for bad salt chars.
- return crypt(secret, config)
- else:
- salt = config[:2]
- return render(salt, raw_crypt(secret, salt))
-
-#=========================================================
-#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())
+#handler
+#=========================================================
+class DesCrypt(BaseHandler):
+ #=========================================================
+ #class attrs
+ #=========================================================
+ name = "des_crypt"
+ setting_kwds = ("salt",)
+ min_salt_chars = max_salt_chars = 2
+
+ #=========================================================
+ #formatting
+ #=========================================================
+ #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
+
+ _pat = re.compile(r"""
+ ^
+ (?P<salt>[./a-z0-9]{2})
+ (?P<chk>[./a-z0-9]{11})?
+ $""", re.X|re.I)
+
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash and cls._pat.match(hash))
+
+ @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 des-crypt hash"
+ salt, chk = m.group("salt", "chk")
+ return cls(salt=salt, checksum=chk, strict=bool(chk))
+
+ def to_string(self):
+ return "%s%s" % (self.salt, self.checksum or '')
+
+ #=========================================================
+ #backend
+ #=========================================================
+ def calc_checksum(self, secret):
+ #forbidding nul chars because linux crypt (and most C implementations) won't accept it either.
+ if '\x00' in secret:
+ raise ValueError, "null char in secret"
+
+ #XXX: des-crypt predates unicode, not sure if there's an official policy for handing it.
+ #for now, just coercing to utf-8.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ #run through chosen backend
+ if crypt:
+ #XXX: given a single letter salt, linux crypt returns a hash with the original salt doubled,
+ # but appears to calculate the hash based on the letter + "G" as the second byte.
+ # this results in a hash that won't validate, which is DEFINITELY wrong.
+ # need to find out it's underlying logic, and if it's part of spec,
+ # or just weirdness that should actually be an error.
+ # until then, passlib raises an error in genconfig()
+
+ #XXX: given salt chars outside of h64.CHARS range, linux crypt
+ # does something unknown when decoding salt to 12 bit int,
+ # successfully creates a hash, but reports the original salt.
+ # need to find out it's underlying logic, and if it's part of spec,
+ # or just weirdness that should actually be an error.
+ # until then, passlib raises an error for bad salt chars.
+ return self.from_string(crypt(secret, self.to_string())).checksum
+ else:
+ return raw_crypt(secret, self.salt)
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+autodocument(DesCrypt)
+register_crypt_handler(DesCrypt)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/ext_des_crypt.py b/passlib/hash/ext_des_crypt.py
index e5a9baa..ab670bc 100644
--- a/passlib/hash/ext_des_crypt.py
+++ b/passlib/hash/ext_des_crypt.py
@@ -8,6 +8,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
+from passlib.base import register_crypt_handler
+from passlib.utils.handlers import BaseHandler
from passlib.utils import norm_rounds, norm_salt, h64, autodocument
from passlib.utils.des import mdes_encrypt_int_block
from passlib.hash.des_crypt import _crypt_secret_to_key
@@ -61,89 +63,72 @@ def raw_ext_crypt(secret, rounds, salt):
#TODO: check if crypt supports ext-des-crypt.
#=========================================================
-#algorithm information
+#handler
#=========================================================
-name = "ext_des_crypt"
-#stats: 64 bit checksum, 24 bit salt, 0..(1<<24)-1 rounds
-
-setting_kwds = ("salt", "rounds")
-context_kwds = ()
-
-min_salt_chars = max_salt_chars = 4
-
-default_rounds = 10000
-min_rounds = 0 #NOTE: some sources (OpenBSD login.conf) report 7250 as minimum allowed rounds
-max_rounds = 16777215 # (1<<24)-1
-
-rounds_cost = "linear"
-
-#=========================================================
-#internal helpers
-#=========================================================
-_pat = re.compile(r"""
- ^
- _
- (?P<rounds>[./a-z0-9]{4})
- (?P<salt>[./a-z0-9]{4})
- (?P<chk>[./a-z0-9]{11})?
- $""", re.X|re.I)
-
-def parse(hash):
- if not hash:
- raise ValueError, "no hash specified"
- m = _pat.match(hash)
- if not m:
- raise ValueError, "invalid ext-des-crypt hash"
- rounds, salt, chk = m.group("rounds", "salt", "chk")
- return dict(
- rounds=h64.decode_int24(rounds),
- salt=salt,
- checksum=chk,
- )
-
-def render(rounds, salt, checksum=None):
- if rounds < 0:
- raise ValueError, "invalid rounds"
- if len(salt) != 4:
- raise ValueError, "invalid salt"
- if checksum and len(checksum) != 11:
- raise ValueError, "invalid checksum"
- return "_%s%s%s" % (h64.encode_int24(rounds), 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
- #TODO: could *easily* optimize this to skip excess render/parse
- info = parse(config)
- info.pop("checksum")
- config = genconfig(**info)
- info = parse(config)
- rounds, salt = info['rounds'], info['salt']
-
- #run through chosen backend
- checksum = raw_ext_crypt(secret, rounds, salt)
- return render(rounds, salt, checksum)
-
-#=========================================================
-#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())
+class ExtDesCrypt(BaseHandler):
+ #=========================================================
+ #class attrs
+ #=========================================================
+ name = "ext_des_crypt"
+ setting_kwds = ("salt", "rounds")
+
+ min_salt_chars = max_salt_chars = 4
+
+ default_rounds = 1000
+ min_rounds = 0
+ max_rounds = 16777215 # (1<<24)-1
+ rounds_cost = "linear"
+
+ checksum_chars = 11
+
+ # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds,
+ # but not sure if that's strictly enforced, or silently clipped.
+
+ #=========================================================
+ #internal helpers
+ #=========================================================
+ _pat = re.compile(r"""
+ ^
+ _
+ (?P<rounds>[./a-z0-9]{4})
+ (?P<salt>[./a-z0-9]{4})
+ (?P<chk>[./a-z0-9]{11})?
+ $""", re.X|re.I)
+
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash and cls._pat.match(hash))
+
+ @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 ext-des-crypt hash"
+ rounds, salt, chk = m.group("rounds", "salt", "chk")
+ return cls(
+ rounds=h64.decode_int24(rounds),
+ salt=salt,
+ checksum=chk,
+ strict=bool(chk),
+ )
+
+ def to_string(self):
+ return "_%s%s%s" % (h64.encode_int24(self.rounds), self.salt, self.checksum or '')
+
+ #=========================================================
+ #backend
+ #=========================================================
+ def calc_checksum(self, secret):
+ return raw_ext_crypt(secret, self.rounds, self.salt)
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+autodocument(ExtDesCrypt)
+register_crypt_handler(ExtDesCrypt, force=True)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/mysql_323.py b/passlib/hash/mysql_323.py
index d4b1917..29b6b30 100644
--- a/passlib/hash/mysql_323.py
+++ b/passlib/hash/mysql_323.py
@@ -18,7 +18,9 @@ from warnings import warn
#site
#libs
#pkg
+from passlib.base import register_crypt_handler
from passlib.utils import autodocument
+from passlib.utils.handlers import PlainHandler
#local
__all__ = [
"genhash",
@@ -31,61 +33,68 @@ __all__ = [
#=========================================================
#backend
#=========================================================
-
-#=========================================================
-#algorithm information
-#=========================================================
-name = "mysql_323"
-#stats: 62 bit checksum, no salt
-
-setting_kwds = ()
-context_kwds = ()
-
-#=========================================================
-#internal helpers
-#=========================================================
-_pat = re.compile(r"^[0-9a-f]{16}$", re.I)
-
-#=========================================================
-#primary interface
-#=========================================================
-def genconfig():
- return None
-
-def genhash(secret, config):
- if config and not identify(config):
- raise ValueError, "not a mysql-323 hash"
-
- MASK_32 = 0xffffffff
- MASK_31 = 0x7fffffff
-
- nr1 = 0x50305735
- nr2 = 0x12345671
- add = 7
- for c in secret:
- if c in ' \t':
- continue
- tmp = ord(c)
- nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32
- nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32
- add = (add+tmp) & MASK_32
- return "%08x%08x" % (nr1 & MASK_31, nr2 & MASK_31)
-
-#=========================================================
-#secondary interface
-#=========================================================
-def encrypt(secret, **settings):
- return genhash(secret, genconfig(**settings))
-
-def verify(secret, hash):
- if not hash:
- raise ValueError, "no hash specified"
- return hash.lower() == genhash(secret, hash)
-
-def identify(hash):
- return bool(hash and _pat.match(hash))
-
-autodocument(globals())
+class MySQL_323(PlainHandler):
+
+ #=========================================================
+ #class attrs
+ #=========================================================
+ name = "mysql_323"
+
+ #=========================================================
+ #init
+ #=========================================================
+ @classmethod
+ def norm_checksum(cls, chk, strict=False):
+ if chk:
+ return chk.lower() #to make upper-case strings verify properly
+ return None
+
+ #=========================================================
+ #formatting
+ #=========================================================
+ _pat = re.compile(r"^[0-9a-f]{16}$", re.I)
+
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash and cls._pat.match(hash))
+
+ @classmethod
+ def from_string(cls, hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = cls._pat.match(hash)
+ if not m:
+ raise ValueError, "not a recognized mysql-323 hash"
+ return cls(checksum=hash)
+
+ def to_string(self):
+ return self.checksum
+
+ #=========================================================
+ #backend
+ #=========================================================
+ def calc_checksum(self, secret):
+ MASK_32 = 0xffffffff
+ MASK_31 = 0x7fffffff
+
+ nr1 = 0x50305735
+ nr2 = 0x12345671
+ add = 7
+ for c in secret:
+ if c in ' \t':
+ continue
+ tmp = ord(c)
+ nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32
+ nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32
+ add = (add+tmp) & MASK_32
+ return "%08x%08x" % (nr1 & MASK_31, nr2 & MASK_31)
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+autodocument(MySQL_323)
+register_crypt_handler(MySQL_323)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/phpass.py b/passlib/hash/phpass.py
index a771758..5351155 100644
--- a/passlib/hash/phpass.py
+++ b/passlib/hash/phpass.py
@@ -16,6 +16,8 @@ from warnings import warn
#site
#libs
from passlib.utils import norm_rounds, norm_salt, h64, autodocument
+from passlib.utils.handlers import BaseHandler
+from passlib.base import register_crypt_handler
#pkg
#local
__all__ = [
@@ -27,133 +29,113 @@ __all__ = [
]
#=========================================================
-#algorithm information
+#phpass
#=========================================================
-name = "phpass"
-#stats: 128 bit checksum, 24 bit salt
-
-setting_kwds = ("salt", "rounds")
-context_kwds = ()
-
-min_salt_chars = max_salt_chars = 8
-
-default_rounds = 9
-min_rounds = 7
-max_rounds = 30
-
-rounds_cost = "log2"
-
-#=========================================================
-#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 be 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 ``H`` in order 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))
-
-autodocument(globals())
+class PHPass(BaseHandler):
+
+ #=========================================================
+ #class attrs
+ #=========================================================
+ name = "phpass"
+ setting_kwds = ("salt", "rounds", "ident")
+
+ min_salt_chars = max_salt_chars = 8
+
+ default_rounds = 9
+ min_rounds = 7
+ max_rounds = 30
+ rounds_cost = "log2"
+
+ _strict_rounds_bounds = True
+ _extra_init_settings = ("ident",)
+
+ #=========================================================
+ #instance attrs
+ #=========================================================
+ ident = None
+
+ #=========================================================
+ #init
+ #=========================================================
+ @classmethod
+ def norm_ident(cls, ident, strict=False):
+ if not ident:
+ if strict:
+ raise ValueError, "no ident specified"
+ ident = "P"
+ if ident not in ("P", "H"):
+ raise ValueError, "invalid ident: %r" % (ident,)
+ return ident
+
+ #=========================================================
+ #formatting
+ #=========================================================
+
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash) and (hash.startswith("$P$") or hash.startswith("$H$"))
+
+ #$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)
+
+ @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 phpass portable hash"
+ ident, rounds, salt, chk = m.group("ident", "rounds", "salt", "chk")
+ return cls(
+ ident=ident,
+ rounds=h64.decode_6bit(rounds),
+ salt=salt,
+ checksum=chk,
+ strict=bool(chk),
+ )
+
+ def to_string(self):
+ return "$%s$%s%s%s" % (self.ident, h64.encode_6bit(self.rounds), self.salt, self.checksum or '')
+
+ #=========================================================
+ #backend
+ #=========================================================
+ def calc_checksum(self, secret):
+ #FIXME: can't find definitive policy on how phpass handles non-ascii.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ real_rounds = 1<<self.rounds
+ result = md5(self.salt + secret).digest()
+ r = 0
+ while r < real_rounds:
+ result = md5(result + secret).digest()
+ r += 1
+ return h64.encode_bytes(result)
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+autodocument(PHPass, settings_doc="""
+:param ident:
+ phpBB3 uses ``H`` instead of ``P`` for it's identifier,
+ this may be set to ``H`` in order to generate phpBB3 compatible hashes.
+ it defaults to ``P``.
+""")
+register_crypt_handler(PHPass)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/sha1_crypt.py b/passlib/hash/sha1_crypt.py
index 00f5c58..53f6b0d 100644
--- a/passlib/hash/sha1_crypt.py
+++ b/passlib/hash/sha1_crypt.py
@@ -18,7 +18,7 @@ except ImportError:
_EVP = None
#libs
from passlib.utils import norm_rounds, norm_salt, autodocument, h64
-from passlib.utils.handlers import BaseSRHandler
+from passlib.utils.handlers import BaseHandler
from passlib.base import register_crypt_handler
#pkg
#local
@@ -45,16 +45,13 @@ if _EVP:
#=========================================================
#sha1-crypt
#=========================================================
-class sha1_crypt(BaseSRHandler):
+class SHA1Crypt(BaseHandler):
#=========================================================
- #algorithm information
+ #class attrs
#=========================================================
name = "sha1_crypt"
- #stats: ?? bit checksum, ?? bit salt, 2**(4..31) rounds
-
setting_kwds = ("salt", "rounds")
- context_kwds = ()
default_salt_chars = 8
min_salt_chars = 0
@@ -66,14 +63,18 @@ class sha1_crypt(BaseSRHandler):
rounds_cost = "linear"
#=========================================================
- #internal helpers
+ #formatting
#=========================================================
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash) and hash.startswith("$sha1$")
+
_pat = re.compile(r"""
^
\$sha1
\$(?P<rounds>\d+)
\$(?P<salt>[A-Za-z0-9./]{0,64})
- (\$(?P<chk>[A-Za-z0-9./]{28}))?
+ (\$(?P<chk>[A-Za-z0-9./]{28})?)?
$
""", re.X)
@@ -91,6 +92,7 @@ class sha1_crypt(BaseSRHandler):
rounds=int(rounds),
salt=salt,
checksum=chk,
+ strict=bool(chk),
)
def to_string(self):
@@ -100,22 +102,18 @@ class sha1_crypt(BaseSRHandler):
return out
#=========================================================
- #primary interface
+ #backend
#=========================================================
-
- #genconfig - uses default method that wraps cls() + to_string()
- #genhash - uses default method that wraps from_string() + calc_checksum() + to_string()
-
def calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
rounds = self.rounds
- result = self.salt + "$sha1$" + str(rounds)
+ result = "%s$sha1$%s" % (self.salt, rounds)
r = 0
while r < rounds:
result = hmac_sha1(secret, result)
r += 1
- return h64.encode_transposed_bytes(result, cls._chk_offsets)
+ return h64.encode_transposed_bytes(result, self._chk_offsets)
_chk_offsets = [
2,1,0,
@@ -128,20 +126,11 @@ class sha1_crypt(BaseSRHandler):
]
#=========================================================
- #secondary interface
- #=========================================================
- def identify(hash):
- return bool(hash) and hash.startswith("$sha1$")
-
- #encrypt - use default method that wraps cls() + calc_checksum() + to_string()
- #verify - use default method that uses equality + from_string() + calc_checksum()
-
- #=========================================================
#eoc
#=========================================================
-autodocument(sha1_crypt)
-register_crypt_handler(sha1_crypt)
+autodocument(SHA1Crypt)
+register_crypt_handler(SHA1Crypt)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/hash/sha512_crypt.py b/passlib/hash/sha512_crypt.py
index a6f2aee..62d5789 100644
--- a/passlib/hash/sha512_crypt.py
+++ b/passlib/hash/sha512_crypt.py
@@ -16,7 +16,7 @@ from warnings import warn
#libs
from passlib.utils import norm_rounds, norm_salt, h64, autodocument
from passlib.hash.sha256_crypt import raw_sha_crypt
-from passlib.utils.handlers import BaseSRHandler
+from passlib.utils.handlers import BaseHandler
from passlib.base import register_crypt_handler
#pkg
#local
@@ -92,7 +92,7 @@ crypt = None
#=========================================================
#sha 512 crypt
#=========================================================
-class sha512_crypt(BaseSRHandler):
+class SHA512Crypt(BaseHandler):
#=========================================================
#algorithm information
@@ -100,7 +100,6 @@ class sha512_crypt(BaseSRHandler):
name = "sha512_crypt"
setting_kwds = ("salt", "rounds")
- context_kwds = ()
min_salt_chars = 0
max_salt_chars = 16
@@ -111,8 +110,23 @@ class sha512_crypt(BaseSRHandler):
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)
- #regexp used to recognize & parse hashes
+ #=========================================================
+ #parsing
+ #=========================================================
+ @classmethod
+ def identify(cls, hash):
+ return bool(hash) and hash.startswith("$6$")
+
+ #: regexp used to parse hashes
_pat = re.compile(r"""
^
\$6
@@ -128,71 +142,6 @@ class sha512_crypt(BaseSRHandler):
$
""", re.X)
- #=========================================================
- #
- #=========================================================
- def __init__(self, implicit_rounds=None, **kwds):
- self.__super = super(sha512_crypt, self)
- self.__super.__init__(**kwds)
- self.implicit_rounds = implicit_rounds
-
- #=========================================================
- #password hash api - primary interface
- #=========================================================
- @classmethod
- def genconfig(cls, salt=None, rounds=None, implicit_rounds=True):
- """generate sha512-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:
- sha512-crypt configuration string.
- """
- return cls(salt=salt, rounds=rounds, implicit_rounds=implicit_rounds).to_string()
-
- @classmethod
- def genhash(cls, secret, config):
- #parse and run through genconfig to validate configuration
- self = cls.from_string(config)
-
- #run through chosen backend
- if crypt:
- #using system's crypt routine.
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return crypt(secret, config)
- else:
- #using builtin routine
- self.checksum, self.salt, self.rounds = raw_sha512_crypt(secret, self.salt, self.rounds)
- return self.to_string()
-
- #=========================================================
- #password hash api - secondary interface
- #=========================================================
- @classmethod
- def identify(cls, hash):
- return bool(hash) and hash.startswith("$6$")
-
- #encrypt - use default method that wraps genconfig + genhash
- #verify - use default method that uses equality + genhash
-
- #=========================================================
- #password hash api - parsing interface
- #=========================================================
@classmethod
def from_string(cls, hash):
if not hash:
@@ -211,19 +160,39 @@ class sha512_crypt(BaseSRHandler):
strict=bool(chk),
)
- def to_string(self): #, rounds, salt, checksum=None, implicit_rounds=True):
- assert '$' not in self.salt
+ def to_string(self):
if self.rounds == 5000 and self.implicit_rounds:
return "$6$%s$%s" % (self.salt, self.checksum or '')
else:
return "$6$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '')
#=========================================================
+ #backend
+ #=========================================================
+ def calc_checksum(self, secret):
+ #run through chosen backend
+ if crypt:
+ #using system's crypt routine.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ return self.from_string(crypt(secret, self.to_string())).checksum
+ else:
+ #using builtin routine
+ checksum, salt, rounds = raw_sha512_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
+
+ #=========================================================
#eoc
#=========================================================
-autodocument(sha512_crypt)
-register_crypt_handler(sha512_crypt)
+ ##:param implicit_rounds:
+ ##
+ ## this is an internal option which generally doesn't need to be touched.
+
+autodocument(SHA512Crypt)
+register_crypt_handler(SHA512Crypt)
#=========================================================
#eof
#=========================================================
diff --git a/passlib/tests/test_hash_des_crypt.py b/passlib/tests/test_hash_des_crypt.py
index 78b73a8..6659138 100644
--- a/passlib/tests/test_hash_des_crypt.py
+++ b/passlib/tests/test_hash_des_crypt.py
@@ -20,7 +20,7 @@ log = getLogger(__name__)
#=========================================================
class DesCryptTest(_HandlerTestCase):
"test DesCrypt algorithm"
- handler = mod
+ handler = mod.DesCrypt
secret_chars = 8
#TODO: test
@@ -35,10 +35,10 @@ class DesCryptTest(_HandlerTestCase):
('AlOtBsOl', 'cEpWz5IUCShqM'),
(u'hell\u00D6', 'saykDgk3BPZ9E'),
)
- known_invalid = (
+ known_invalid = [
#bad char in otherwise correctly formatted hash
'!gAwTx2l6NADI',
- )
+ ]
if mod.backend != "builtin" and enable_option("all-backends"):
@@ -56,7 +56,7 @@ if mod.backend != "builtin" and enable_option("all-backends"):
class ExtDesCryptTest(_HandlerTestCase):
"test ExtDesCrypt algorithm"
- handler = mod2
+ handler = mod2.ExtDesCrypt
known_correct = (
(" ", "_K1..crsmZxOLzfJH8iw"),
("my", '_KR/.crsmykRplHbAvwA'), #<- to detect old 12-bit rounds bug
diff --git a/passlib/tests/test_hash_misc.py b/passlib/tests/test_hash_misc.py
index ef5aa64..f0742c6 100644
--- a/passlib/tests/test_hash_misc.py
+++ b/passlib/tests/test_hash_misc.py
@@ -19,7 +19,7 @@ log = getLogger(__name__)
from passlib.hash import phpass
class PHPassTest(_HandlerTestCase):
- handler = phpass
+ handler = phpass.PHPass
known_correct = (
('', '$P$7JaFQsPzJSuenezefD/3jHgt5hVfNH0'),
@@ -27,10 +27,10 @@ class PHPassTest(_HandlerTestCase):
('test12345', '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0'), #from the source
)
- known_invalid = (
+ known_identified_invalid = [
#bad char in otherwise correct hash
'$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r!L0',
- )
+ ]
#=========================================================
#NTHASH for unix
@@ -56,17 +56,17 @@ class NTHashTest(_HandlerTestCase):
from passlib.hash import sha1_crypt
class SHA1CryptTest(_HandlerTestCase):
- handler = sha1_crypt.sha1_crypt
+ handler = sha1_crypt.SHA1Crypt
known_correct = (
("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"),
("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"),
)
- known_invalid = (
+ known_identified_invalid = [
#bad char in otherwise correct hash
'$sha1$21773$u!7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH',
- )
+ ]
#=========================================================
#EOF
diff --git a/passlib/tests/test_hash_mysql.py b/passlib/tests/test_hash_mysql.py
index d0f97dd..e65544b 100644
--- a/passlib/tests/test_hash_mysql.py
+++ b/passlib/tests/test_hash_mysql.py
@@ -17,19 +17,19 @@ log = getLogger(__name__)
#=========================================================
#database hashes
#=========================================================
-class Mysql323CryptTest(_HandlerTestCase):
- handler = mod3
+class MySQL_323Test(_HandlerTestCase):
+ handler = mod3.MySQL_323
- #remove single space from secrets, since mysql-10 DISCARDS WHITESPACE !?!
+ #remove single space from secrets, since mysql-323 ignores all whitespace (?!)
standard_secrets = [ x for x in _HandlerTestCase.standard_secrets if x != ' ' ]
known_correct = (
('mypass', '6f8c114b58f2ce9e'),
)
- known_invalid = (
+ known_invalid = [
#bad char in otherwise correct hash
'6z8c114b58f2ce9e',
- )
+ ]
def test_whitespace(self):
"check whitespace is ignored per spec"
diff --git a/passlib/tests/test_hash_sha_crypt.py b/passlib/tests/test_hash_sha_crypt.py
index 169a414..45dd897 100644
--- a/passlib/tests/test_hash_sha_crypt.py
+++ b/passlib/tests/test_hash_sha_crypt.py
@@ -63,8 +63,8 @@ if mod2.backend != "builtin" and enable_option("all-backends"):
#=========================================================
#test sha512-crypt
#=========================================================
-class Sha512CryptTest(_HandlerTestCase):
- handler = mod5.sha512_crypt
+class SHA512CryptTest(_HandlerTestCase):
+ handler = mod5.SHA512Crypt
supports_unicode = True
known_correct = (
@@ -74,15 +74,13 @@ class Sha512CryptTest(_HandlerTestCase):
('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'),
('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'),
)
- known_invalid = (
- #bad char in otherwise correct hash
- '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1',
- )
- known_identified_invalid = (
- ###zero-padded rounds
+ known_identified_invalid = [
+ #zero-padded rounds
'$6$rounds=011021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1',
- )
+ #bad char in otherwise correct hash
+ '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1',
+ ]
#NOTE: these test cases taken from spec definition at http://www.akkadia.org/drepper/SHA-crypt.txt
cases512 = [
@@ -121,7 +119,7 @@ class Sha512CryptTest(_HandlerTestCase):
def test_spec_vectors(self):
"verify sha512-crypt passes specification test vectors"
- handler = mod5
+ handler = self.handler
#NOTE: the 'roundstoolow' vector is known to raise a warning, which we silence here
if catch_warnings:
@@ -130,22 +128,13 @@ class Sha512CryptTest(_HandlerTestCase):
warnings.filterwarnings("ignore", "sha512_crypt algorithm does not allow less than 1000 rounds: 10")
for config, secret, hash in self.cases512:
-
- result = handler.genhash(secret, config)
-
- #parse config
- settings = handler.parse(config)
-
#make sure we got expected result back
+ result = handler.genhash(secret, config)
self.assertEqual(result, hash, "hash=%r secret=%r:" % (hash, secret))
- #parse result and check that salt was truncated to max 16 chars
- info = handler.parse(result)
- if len(settings['salt']) > 16:
- #spec sez we can truncate salt
- self.assertEqual(info['salt'], settings['salt'][:16], "hash=%r secret=%r:" % (hash, secret))
- else:
- self.assertEqual(info['salt'], settings['salt'], "hash=%r secret=%r:" % (hash, secret))
+ #make sure parser truncated salts
+ info = handler.from_string(config)
+ self.assert_(len(info.salt) <= 16, "hash=%r secret=%r:" % (hash, secret))
if catch_warnings:
ctx.__exit__(None,None,None)
@@ -154,7 +143,7 @@ if mod5.backend != "builtin" and enable_option("all-backends"):
#monkeypatch sha512-crypt mod so it uses builtin backend
- class BuiltinSha512CryptTest(Sha512CryptTest):
+ class BuiltinSHA512CryptTest(SHA512CryptTest):
case_prefix = "sha512-crypt (builtin backend)"
def setUp(self):
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 4cca5f1..cb42003 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -435,7 +435,7 @@ class dict_proxy(object):
except KeyError:
raise AttributeError, "attribute not found: %r" % (key,)
-def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''):
+def autodocument(scope, salt_charset="[./0-9A-Za-z]", settings_doc='', context_doc=''):
"""helper to auto-generate documentation for password hash handler
:arg scope: dict containing encrypt/verify/etc functions (module scope or class dict)
@@ -468,6 +468,8 @@ def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''):
if context_doc:
context_doc = context_doc.rstrip() + "\n"
+ if settings_doc:
+ settings_doc = settings_doc.rstrip() + "\n"
def get_func(name):
func = getattr(scope, name)
@@ -481,7 +483,7 @@ def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''):
genconfig = get_func("genconfig")
if not genconfig.__doc__:
if setting_kwds:
- if has_other:
+ if has_other and not settings_doc:
raise NotImplementedError, "can't auto generate genconfig docs w/ unknown setting_kwds"
d = "generate %(name)s configuration string\n\n" % dict(name=name)
@@ -500,6 +502,9 @@ def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''):
if log_rounds:
d += """ %(name)s's rounds value is logarithmic, the actual number of rounds used is ``2**rounds``.\n""" % dict(name=name)
+ if settings_doc:
+ d += settings_doc + "\n"
+
d += """\n:raises ValueError: if invalid settings are passed in\n\n"""
d += """:returns:\n %(name)s configuration string\n""" % dict(name=name)
else:
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index 7e38108..a515d4e 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -19,338 +19,156 @@ from passlib.utils import abstractmethod, abstractclassmethod, classproperty, h6
__all__ = [
#framework for implementing handlers
- 'CryptHandler',
- 'ExtCryptHandler',
+ 'BaseHandler',
+ 'PlainHandler',
]
-#==========================================================
-#base interface for all the crypt algorithm implementations
-#==========================================================
-class CryptHandler(object):
- """helper class for implementing a password algorithm using class methods"""
-
- #=========================================================
- #class attrs
- #=========================================================
-
- name = None #globally unique name to identify algorithm. should be lower case and hyphens only
- context_kwds = () #tuple of additional kwds required for any encrypt / verify operations; eg "realm" or "user"
- setting_kwds = () #tuple of additional kwds that encrypt accepts for configuration algorithm; eg "salt" or "rounds"
-
- #=========================================================
- #primary interface - primary methods implemented by each handler
- #=========================================================
-
- @abstractclassmethod
- def genhash(cls, secret, config, **context):
- """encrypt secret to hash"""
-
- @classmethod
- def genconfig(cls, **settings):
- """return configuration string encoding settings for hash generation"""
- #NOTE: this implements a default method which is suitable ONLY for classes with no configuration.
- if cls.setting_kwds:
- raise NotImplementedError, "classes with config kwds must implement genconfig()"
- if settings:
- raise TypeError, "%s has no configuration options" % (cls,)
- return None
-
- #=========================================================
- #secondary interface - more useful interface for user,
- # frequently implemented more efficiently by specific handlers
- #=========================================================
-
- @classmethod
- def identify(cls, hash):
- """identify if a hash string belongs to this algorithm."""
- #NOTE: this default method is going to be *really* slow for most implementations,
- #they should override it. but if genhash() conforms to the specification, this will do.
- if cls.context_kwds:
- raise NotImplementedError, "classes with context kwds must implement identify()"
- if not hash:
- return False
- try:
- cls.genhash("stub", hash)
- except ValueError:
- return False
- return True
-
- @classmethod
- def encrypt(cls, secret, **kwds):
- """encrypt secret, returning resulting hash string."""
- if cls.context_kwds:
- context = dict(
- (k,kwds.pop(k))
- for k in cls.context_kwds
- if k in kwds
- )
- config = cls.genconfig(**kwds)
- return cls.genhash(secret, config, **context)
- else:
- config = cls.genconfig(**kwds)
- return cls.genhash(secret, config)
-
- @classmethod
- def verify(cls, secret, hash, **context):
- """verify a secret against an existing hash."""
- #NOTE: methods whose hashes have multiple encodings should override this,
- # as the hash will need to be normalized before comparing via string equality.
- # alternately, the ExtCryptHandler class provides a more flexible framework.
-
- #ensure hash was specified - genhash() won't throw error for this
- if not hash:
- raise ValueError, "no hash specified"
-
- #the genhash() implementation for most setting-less algorithms
- #simply ignores the config string provided; whereas most
- #algorithms with settings have to inspect and validate it.
- #therefore, we do this quick check IFF it's setting-less
- if not cls.setting_kwds and not cls.identify(hash):
- raise ValueError, "not a %s hash" % (cls.name,)
-
- #do simple string comparison
- return hash == cls.genhash(secret, hash, **context)
-
- #=========================================================
- #eoc
- #=========================================================
-
-#=========================================================
-#
-#=========================================================
-##class ExtCryptHandler(CryptHandler):
-## """class providing an extended handler interface,
-## allowing manipulation of hash & config strings.
-##
-## this extended interface adds methods for parsing and rendering
-## a hash or config string to / from a dictionary of components.
-##
-## this interface is generally easier to use when *implementing* hash
-## algorithms, and as such is used through passlib. it's kept separate
-## from :class:`CryptHandler` itself, since it's features are not typically
-## required for user-facing purposes.
-##
-## when implementing a hash algorithm, subclasses must implement:
-##
-## * parse()
-## * render()
-## * genconfig() - render, _norm_salt, _norm_rounds usually helpful for this
-## * genhash() - parse, render usually helpful for this
-##
-## subclasses may optionally implement more efficient versions of
-## these functions, though the defaults should be sufficient:
-##
-## * identify() - requires parse()
-## * verify() - requires parse()
-##
-## some helper methods are provided for implementing genconfig, genhash & verify.
-## """
+###==========================================================
+###base interface for all the crypt algorithm implementations
+###==========================================================
+##class CryptHandler(object):
+## """helper class for implementing a password algorithm using class methods"""
##
## #=========================================================
## #class attrs
## #=========================================================
##
-## #---------------------------------------------------------
-## # _norm_salt() configuration
-## #---------------------------------------------------------
-##
-## salt_chars = None #fill in with (maxium) number of salt chars required, and _norm_salt() will handle truncating etc
-## salt_charset = h64.CHARS #helper used when generating salt
-## salt_charpat = None #optional regexp used by _norm_salt to validate salts
-##
-## #override only if minimum number of salt chars is different from salt_chars
-## @classproperty
-## def min_salt_chars(cls):
-## return cls.salt_chars
-##
-## #---------------------------------------------------------
-## #_norm_rounds() configuration
-## #---------------------------------------------------------
-## default_rounds = None #default number of rounds to use if none specified (can be name of a preset)
-## min_rounds = None #minimum number of rounds (smaller values silently ignored)
-## max_rounds = None #maximum number of rounds (larger values silently ignored)
+## name = None #globally unique name to identify algorithm. should be lower case and hyphens only
+## context_kwds = () #tuple of additional kwds required for any encrypt / verify operations; eg "realm" or "user"
+## setting_kwds = () #tuple of additional kwds that encrypt accepts for configuration algorithm; eg "salt" or "rounds"
##
## #=========================================================
-## #backend parsing routines - used by helpers below
+## #primary interface - primary methods implemented by each handler
## #=========================================================
##
## @abstractclassmethod
-## def parse(cls, hash):
-## """parse hash or config into dictionary.
-##
-## :arg hash: the hash/config string to parse
-##
-## :raises ValueError:
-## If hash/config string is empty,
-## or not recognized as belonging to this algorithm
-##
-## :returns:
-## dictionary containing a subset of the keys
-## specified in :attr:`setting_kwds`.
-##
-## commonly used keys are ``salt``, ``rounds``.
-##
-## If and only if the string is a hash, the dict should also contain
-## the key ``checksum``, mapping to the checksum portion of the hash.
-##
-## .. note::
-## Specific implementations may perform anywhere from none to full
-## validation of input string; the primary goal of this method
-## is to parse settings from single string into kwds
-## which will be recognized by :meth:`render` and :meth:`encrypt`.
-##
-## :meth:`encrypt` is where validation of inputs *must* be performed.
-##
-## .. note::
-## If multiple encoding formats are possible, this *must* normalize
-## the checksum kwd to it's canonical format, so the default
-## verify() method can work properly.
-## """
+## def genhash(cls, secret, config, **context):
+## """encrypt secret to hash"""
##
-## @abstractclassmethod
-## def render(cls, checksum=None, **settings):
-## """render hash from checksum & settings (as returned by :meth:`parse`).
-##
-## :param checksum:
-## Encoded checksum portion of hash.
-##
-## :param settings:
-## All other keywords are algorithm-specified,
-## and should be listed in :attr:`setting_kwds`.
-##
-## :raises ValueError:
-## If any values are not encodeable into hash.
-##
-## :raises NotImplementedError:
-## If checksum is omitted and the algorithm
-## doesn't have any settings (:attr:`setting_kwds` is empty),
-## or doesn't support generating "salt strings"
-## which contain all configuration except for the
-## checksum itself.
-##
-## :returns:
-## if checksum is specified, this should return a fully-formed hash.
-## otherwise, it should return a config string containing
-## the specified inputs.
-##
-## .. note::
-## Specific implementations may perform anywhere from none to full
-## validation of inputs; the primary goal of this method
-## is to render the settings into a single string
-## which will be recognized by :meth:`parse`.
-##
-## :meth:`encrypt` is where validation of inputs *must* be performed.
-## """
-##
-## #=========================================================
-## #genhash helper functions
-## #=========================================================
-##
-## #NOTE: genhash() must be implemented,
-## # but helper functions are provided below for common workflows...
-##
-## #----------------------------------------------------------------
-## #for handlers which normalize config string and hand off to external library
-## #----------------------------------------------------------------
## @classmethod
-## def _norm_config(cls, config):
-## """normalize & validate config string"""
-## assert cls.setting_kwds, "_norm_config not designed for hashses w/o settings"
-## if not config:
-## raise ValueError, "no %s hash or config string specified" % (cls.name,)
-## settings = cls.parse(config) #this should catch malformed entries
-## settings.pop("checksum", None) #remove checksum if a hash was passed in
-## return cls.genconfig(**settings) #re-generate config string, let genconfig() catch invalid values
-##
-## #----------------------------------------------------------------
-## #for handlers which implement the guts of the process directly
-## #----------------------------------------------------------------
-##
-## # render() is also usually used for implementing genhash() in this case
-##
-## @classmethod
-## def _parse_norm_config(cls, config):
-## """normalize & validate config string, return parsed dictionary"""
-## return cls.parse(cls._norm_config(config))
-##
-## #=========================================================
-## #genconfig helpers
-## #=========================================================
-##
-## #NOTE: genconfig() must still be implemented,
-## # but helper functions provided below
-##
-## #render() is usually used for implementing genconfig()
-##
-## @classmethod
-## def _norm_rounds(cls, rounds):
-## return norm_rounds(rounds, cls.default_rounds, cls.min_rounds, cls.max_rounds, name=cls.name)
-##
-## @classmethod
-## def _norm_salt(cls, salt):
-## return norm_salt(salt, cls.min_salt_chars, cls.salt_chars, cls.salt_charset, name=cls.name)
+## def genconfig(cls, **settings):
+## """return configuration string encoding settings for hash generation"""
+## #NOTE: this implements a default method which is suitable ONLY for classes with no configuration.
+## if cls.setting_kwds:
+## raise NotImplementedError, "classes with config kwds must implement genconfig()"
+## if settings:
+## raise TypeError, "%s has no configuration options" % (cls,)
+## return None
##
## #=========================================================
-## #identify helpers
+## #secondary interface - more useful interface for user,
+## # frequently implemented more efficiently by specific handlers
## #=========================================================
##
-## #NOTE: this default identify implementation is usually sufficient
-## # (and better than CryptHandler.identify),
-## # though implementations may override it with an even faster check,
-## # such as just looking for a specific string prefix & size
-##
## @classmethod
## def identify(cls, hash):
+## """identify if a hash string belongs to this algorithm."""
+## #NOTE: this default method is going to be *really* slow for most implementations,
+## #they should override it. but if genhash() conforms to the specification, this will do.
+## if cls.context_kwds:
+## raise NotImplementedError, "classes with context kwds must implement identify()"
+## if not hash:
+## return False
## try:
-## cls.parse(hash)
+## cls.genhash("stub", hash)
## except ValueError:
## return False
## return True
##
-## #=========================================================
-## #encrypt helper functions
-## #=========================================================
-##
-## #NOTE: the default encrypt() method very rarely needs overidding at all.
-##
-## #=========================================================
-## #verify helper functions
-## #=========================================================
-##
-## #NOTE: the default verify method provided here works for most cases,
-## # though some handlers will want to implement norm_hash() if their
-## # hash has multiple equivalent representations (eg: case insensitive)
+## @classmethod
+## def encrypt(cls, secret, **kwds):
+## """encrypt secret, returning resulting hash string."""
+## if cls.context_kwds:
+## context = dict(
+## (k,kwds.pop(k))
+## for k in cls.context_kwds
+## if k in kwds
+## )
+## config = cls.genconfig(**kwds)
+## return cls.genhash(secret, config, **context)
+## else:
+## config = cls.genconfig(**kwds)
+## return cls.genhash(secret, config)
##
## @classmethod
-## def verify(cls, secret, hash, **context_kwds):
-## info = cls.parse(hash) #<- should throw ValueError for us if hash is invalid
-## if not info.get('checksum'):
-## raise ValueError, "hash lacks checksum (did you pass a config string into verify?)"
-## other_hash = cls.genhash(secret, hash, **context_kwds)
-## other_info = cls.parse(other_hash)
-## return info['checksum'] == other_info['checksum']
+## def verify(cls, secret, hash, **context):
+## """verify a secret against an existing hash."""
+## #NOTE: methods whose hashes have multiple encodings should override this,
+## # as the hash will need to be normalized before comparing via string equality.
+## # alternately, the ExtCryptHandler class provides a more flexible framework.
+##
+## #ensure hash was specified - genhash() won't throw error for this
+## if not hash:
+## raise ValueError, "no hash specified"
+##
+## #the genhash() implementation for most setting-less algorithms
+## #simply ignores the config string provided; whereas most
+## #algorithms with settings have to inspect and validate it.
+## #therefore, we do this quick check IFF it's setting-less
+## if not cls.setting_kwds and not cls.identify(hash):
+## raise ValueError, "not a %s hash" % (cls.name,)
+##
+## #do simple string comparison
+## return hash == cls.genhash(secret, hash, **context)
##
## #=========================================================
## #eoc
## #=========================================================
#=========================================================
-#
+# BaseHandler
+# rounds+salt+xtra phpass, sha256_crypt, sha512_crypt
+# rounds+salt bcrypt, ext_des_crypt, sha1_crypt, sun_md5_crypt
+# salt only apr_md5_crypt, des_crypt, md5_crypt
#=========================================================
-class BaseSRHandler(object):
- """helper class for implementing hash schemes which have both salt and rounds"""
+class BaseHandler(object):
+ """helper class for implementing hash schemes
+
+ hash implementations should fill out the following:
+ * all required class attributes
+ - name, setting_kwds
+ - max_salt_chars, min_salt_chars, etc - only if salt is used
+ - max_rounds, min_rounds, default_roudns - only if rounds are used
+ * classmethod from_string()
+ * instancemethod to_string()
+ * instancemethod calc_checksum()
+
+ many implementations will want to override the following:
+ * classmethod identify() can usually be done more efficiently
+ * checksum_charset, checksum_chars attributes may prove helpful for validation
+
+ most implementations can use defaults for the following:
+ * genconfig(), genhash(), encrypt(), verify(), etc
+ * norm_checksum() usually only needs overriding if checksum has multiple encodings
+
+ note this class does not support context kwds of any type,
+ since that is a rare enough requirement inside passlib.
+
+ implemented subclasses may call cls.validate_class() to check attribute consistency
+ (usually only required in unittests, etc)
+ """
#=========================================================
- #password hash api - required attributes
+ #class attributes
#=========================================================
- name = None #required
- setting_kwds = ("salt", "rounds")
+
+ #----------------------------------------------
+ #password hash api - required attributes
+ #----------------------------------------------
+ name = None #required by BaseHandler
+ setting_kwds = None #required by BaseHandler
context_kwds = ()
- #=========================================================
- #password hash api - optional salt attributes
- #=========================================================
- max_salt_chars = None #required
+ #----------------------------------------------
+ #checksum information
+ #----------------------------------------------
+ checksum_charset = None #if specified, norm_checksum() will validate this
+ checksum_chars = None #if specified, norm_checksum will require this length
+
+ #----------------------------------------------
+ #salt information
+ #----------------------------------------------
+ max_salt_chars = None #required by BaseHandler.norm_salt()
@classproperty
def min_salt_chars(cls):
@@ -362,97 +180,131 @@ class BaseSRHandler(object):
"default salt chars (defaults to max_salt_chars if not specified by subclass)"
return cls.max_salt_chars
- salt_chars = h64.CHARS
+ salt_charset = h64.CHARS
@classproperty
def default_salt_charset(cls):
- return cls.salt_chars
+ return cls.salt_charset
- #=========================================================
- #password hash api - optional rounds attributes
- #=========================================================
+ #----------------------------------------------
+ #rounds information
+ #----------------------------------------------
min_rounds = 0
- max_rounds = None #required
- default_rounds = None #required
+ max_rounds = None #required by BaseHandler.norm_rounds()
+ default_rounds = None #if not specified, BaseHandler.norm_rounds() will require explicit rounds value every time
rounds_cost = "linear" #common case
- strict_rounds_bounds = False #if true, always raises error if specified rounds values out of range - required by spec for some hashes
+
+ #----------------------------------------------
+ #misc BaseHandler configuration
+ #----------------------------------------------
+ _strict_rounds_bounds = False #if true, always raises error if specified rounds values out of range - required by spec for some hashes
+ _extra_init_settings = () #settings that BaseHandler.__init__ should handle by calling norm_<key>()
#=========================================================
- #other class attrs
+ #instance attributes
#=========================================================
- #TODO: could implement norm_checksum()
- ##checksum_size = None #required - checksum size
- ##checksum_charset = None #required - checksum charset
+ checksum = None
+ salt = None
+ rounds = None
#=========================================================
- #handler helpers
+ #init
#=========================================================
+ #XXX: rename strict kwd to _strict ?
+ 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)
+ self.rounds = self.norm_rounds(rounds, strict=strict)
+ extra = self._extra_init_settings
+ if extra:
+ for key in extra:
+ value = kwds.pop(key, None)
+ norm = getattr(self, "norm_" + key)
+ value = norm(value, strict=strict)
+ setattr(self, key, value)
+ super(BaseHandler, self).__init__(**kwds)
+
@classmethod
def validate_class(cls):
"helper to ensure class is configured property"
if not cls.name:
- raise ValueError, "no name specified"
+ raise AssertionError, "class must have .name attribute set"
- if 'rounds' not in cls.setting_kwds:
- raise ValueError, "rounds not in setting_kwds"
+ if cls.setting_kwds is None:
+ raise AssertionError, "class must have .setting_kwds attribute set"
- if cls.max_rounds is None:
- raise ValueError, "max rounds not specified"
+ if any(k not in cls.setting_kwds for k in cls._extra_init_settings):
+ raise AssertionError, "_extra_init_settings must be subset of setting_kwds"
- if cls.min_rounds > cls.max_rounds:
- raise ValueError, "min rounds too large"
+ if 'salt' in cls.setting_kwds:
- if cls.default_rounds is None:
- raise ValueError, "default rounds not specified"
- if cls.default_rounds < cls.min_rounds:
- raise ValueError, "default rounds too small"
- if cls.default_rounds > cls.max_rounds:
- raise ValueError, "default rounds too large"
+ if cls.min_salt_chars > cls.max_salt_chars:
+ raise AssertionError, "min salt chars too large"
- if cls.rounds_cost not in ("linear", "log2"):
- raise ValueError, "unknown rounds cost function"
+ if cls.default_salt_chars < cls.min_salt_chars:
+ raise AssertionError, "default salt chars too small"
+ if cls.default_salt_chars > cls.max_salt_chars:
+ raise AssertionError, "default salt chars too large"
- if 'salt' not in cls.setting_kwds:
- raise ValueError, "salt not in setting_kwds"
+ if any(c not in cls.salt_charset for c in cls.default_salt_charset):
+ raise AssertionError, "default salt charset not subset of salt charset"
- if cls.min_salt_chars > cls.max_salt_chars:
- raise ValueError, "min salt chars too large"
+ if 'rounds' in cls.setting_kwds:
- if cls.default_salt_chars < cls.min_salt_chars:
- raise ValueError, "default salt chars too small"
- if cls.default_salt_chars > cls.max_salt_chars:
- raise ValueError, "default salt chars too large"
+ if cls.max_rounds is None:
+ raise AssertionError, "max rounds not specified"
- if any(c not in cls.salt_charset for c in cls.default_salt_charset):
- raise ValueError, "default salt charset not subset of salt charset"
+ if cls.min_rounds > cls.max_rounds:
+ raise AssertionError, "min rounds too large"
- #=========================================================
- #instance attributes
- #=========================================================
- checksum = None
- salt = None
- rounds = None
+ if cls.default_rounds is not None:
+ if cls.default_rounds < cls.min_rounds:
+ raise AssertionError, "default rounds too small"
+ if cls.default_rounds > cls.max_rounds:
+ raise AssertionError, "default rounds too large"
- #=========================================================
- #init
- #=========================================================
- def __init__(self, checksum=None, salt=None, rounds=None, strict=False):
- self.checksum = checksum
- self.salt = self.norm_salt(salt, strict=strict)
- self.rounds = self.norm_rounds(rounds, strict=strict)
+ if cls.rounds_cost not in ("linear", "log2"):
+ raise AssertionError, "unknown rounds cost function"
#=========================================================
#helpers
#=========================================================
@classmethod
+ def norm_checksum(cls, checksum, strict=False):
+ if checksum is None:
+ return None
+ cc = cls.checksum_chars
+ if cc and len(checksum) != cc:
+ raise ValueError, "%s checksum must be %d characters" % (cls.name, cc)
+ cs = cls.checksum_charset
+ if cs and any(c not in cs for c in checksum):
+ 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"
+ if not cls._has_salt:
+ if salt is not None:
+ raise ValueError, "%s does not support ``salt``" % (cls.name,)
+ return None
+
if salt is None:
if strict:
raise ValueError, "no salt specified"
return getrandstr(rng, cls.default_salt_charset, cls.default_salt_chars)
+ #TODO: run salt_charset tests
+
mn = cls.min_salt_chars
if mn and len(salt) < mn:
raise ValueError, "%s salt string must be >= %d characters" % (cls.name, mn)
@@ -465,15 +317,31 @@ class BaseSRHandler(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"
+ if not cls._has_rounds:
+ if rounds is not None:
+ raise ValueError, "%s does not support ``rounds``" % (cls.name,)
+ return None
+
if rounds is None:
if strict:
raise ValueError, "no rounds specified"
- return cls.default_rounds
+ rounds = cls.default_rounds
+ if rounds is None:
+ raise ValueError, "%s requires an explicitly-specified rounds value" % (cls.name,)
+ return rounds
- if cls.strict_rounds_bounds:
+ if cls._strict_rounds_bounds:
strict = True
mn = cls.min_rounds
@@ -499,14 +367,13 @@ class BaseSRHandler(object):
@classmethod
def genhash(cls, secret, config):
- if cls.calc_checksum:
- self = cls.from_string(config)
- self.checksum = self.calc_checksum(secret)
- return self.to_string()
- else:
- raise NotImplementedError, "%s subclass must implemented genhash OR calc_checksum" % (cls.name,)
+ self = cls.from_string(config)
+ self.checksum = self.calc_checksum(secret)
+ return self.to_string()
- calc_checksum = None #subclasses may alternately implement this instead of genhash
+ def calc_checksum(self, secret):
+ "given secret; calcuate and return encoded checksum portion of hash string, taking config from object state"
+ raise NotImplementedError, "%s must implement calc_checksum()" % (cls,)
#=========================================================
#password hash api - secondary interface (default implementation)
@@ -526,42 +393,190 @@ class BaseSRHandler(object):
@classmethod
def encrypt(cls, secret, **settings):
self = cls(**settings)
- if self.calc_checksum:
- #save ourselves some parsing calls if subclass provides this method
- self.checksum = self.calc_checksum(secret)
- return self.to_string()
- else:
- return cls.genhash(secret, self.to_string())
+ self.checksum = self.calc_checksum(secret)
+ return self.to_string()
@classmethod
def verify(cls, secret, hash):
- #NOTE: classes may wish to override this
- if cls.calc_checksum:
- #save ourselves some parsing calls if subclass provides this method
- self = cls.from_string(hash)
- return self.checksum == self.calc_checksum(secret)
- else:
- return hash == cls.genhash(secret, hash)
+ #NOTE: classes with multiple checksum encodings (rare)
+ # may wish to either override this, or override norm_checksum
+ # to normalize any checksums provided by from_string()
+ self = cls.from_string(hash)
+ return self.checksum == self.calc_checksum(secret)
#=========================================================
#password hash api - parsing interface
#=========================================================
- @abstractclassmethod
+ @classmethod
def from_string(cls, hash):
"return parsed instance from hash/configuration string; raising ValueError on invalid inputs"
- #MUST BE IMPLEMENTED BY SUBCLASS
- pass
+ raise NotImplementedError, "%s must implement from_string()" % (cls,)
- @abstractmethod
def to_string(self):
"render instance to hash or configuration string (depending on if checksum attr is set)"
- #MUST BE IMPLEMENTED BY SUBCLASS
- pass
+ 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()
#=========================================================
#
#=========================================================
#=========================================================
+#plain - mysql_323, mysql_41, nthash, postgres_md5
+#=========================================================
+class PlainHandler(object):
+ """helper class optimized for implementing hash schemes which have NO settings whatsoever"""
+ #=========================================================
+ #password hash api - required attributes
+ #=========================================================
+ name = None #required
+ setting_kwds = ()
+ context_kwds = ()
+
+ #=========================================================
+ #helpers for norm checksum
+ #=========================================================
+ checksum_charset = None #if specified, norm_checksum() will validate this
+ checksum_chars = None #if specified, norm_checksum will require this length
+
+ #=========================================================
+ #init
+ #=========================================================
+ def __init__(self, checksum=None, strict=False, **kwds):
+ self.checksum = self.norm_checksum(checksum, strict=strict)
+ super(PlainHandler, self).__init__(**kwds)
+
+ @classmethod
+ def validate_class(cls):
+ "helper to validate that class has been configured properly"
+ if not cls.name:
+ raise AssertionError, "class must have .name attribute set"
+
+ #=========================================================
+ #helpers
+ #=========================================================
+ norm_checksum = BaseHandler.norm_checksum.im_func
+
+ #=========================================================
+ #primary interface
+ #=========================================================
+ @classmethod
+ def genconfig(cls):
+ return None
+
+ @classmethod
+ def genhash(cls, secret, config, **context):
+ #NOTE: config is ignored
+ self = cls()
+ self.checksum = self.calc_checksum(secret, **context)
+ return self.to_string()
+
+ calc_checksum = BaseHandler.calc_checksum.im_func
+
+ #=========================================================
+ #secondary 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 encrypt(cls, secret, **context):
+ return cls.genhash(secret, None, **context)
+
+ @classmethod
+ def verify(cls, secret, hash, **context):
+ #NOTE: classes may wish to override this
+ self = cls.from_string(hash)
+ return self.checksum == self.calc_checksum(secret, **context)
+
+ #=========================================================
+ #parser interface
+ #=========================================================
+ @classmethod
+ def from_string(cls, hash):
+ raise NotImplementedError, "implement in subclass"
+
+ def to_string(cls):
+ raise NotImplementedError, "implement in subclass"
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+#=========================================================
+#wrapper
+#=========================================================
+class WrapperHandler(object):
+ "helper for implementing wrapper of crypt-like interface, only required genconfig & genhash"
+
+ #=====================================================
+ #required attributes
+ #=====================================================
+ name = None
+ setting_kwds = None
+ context_kwds = ()
+
+ #=====================================================
+ #required methods
+ #=====================================================
+ @classmethod
+ def genconfig(cls, **settings):
+ raise NotImplementedError, "%s subclass must implement genconfig()" % (cls,)
+
+ @classmethod
+ def genhash(cls, secret, config):
+ raise NotImplementedError, "%s subclass must implement genhash()" % (cls,)
+
+ #=====================================================
+ #default methods (usually subclassed)
+ #=====================================================
+ @classmethod
+ def identify(cls, hash):
+ #NOTE: relying on genhash throwing error for invalid,
+ # but takes a long time for non-invalid.
+ # subclasses *really* should override this.
+ try:
+ cls.genhash('stub', hash)
+ return True
+ except ValueError:
+ return False
+
+ #=====================================================
+ #default methods (rarely subclassed)
+ #=====================================================
+ @classmethod
+ def encrypt(cls, secret, **settings):
+ config = cls.genconfig(**settings)
+ return cls.genhash(secret, config)
+
+ @classmethod
+ def verify(cls, secret, hash):
+ return hash == cls.genhash(secret, hash)
+
+ #=====================================================
+ #eoc
+ #=====================================================
+
+#=========================================================
# eof
#=========================================================