summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-02-09 12:32:11 -0500
committerEli Collins <elic@assurancetechnologies.com>2011-02-09 12:32:11 -0500
commitbdcd1f2997c606994f199f7db45bf52a2a41c1b0 (patch)
treea2aaea7cc1d559d747566748af8bf67bde537de9
parent15f6fe8633ceb47695e05ff1bc0d7b95db0e3075 (diff)
downloadpasslib-bdcd1f2997c606994f199f7db45bf52a2a41c1b0.tar.gz
added netbsd's sha1-crypt (needs doc)
-rw-r--r--docs/notes.txt3
-rw-r--r--passlib/hash/sha1_crypt.py159
-rw-r--r--passlib/tests/test_hash_misc.py18
-rw-r--r--passlib/unix.py4
4 files changed, 182 insertions, 2 deletions
diff --git a/docs/notes.txt b/docs/notes.txt
index 9137e4d..204a4ef 100644
--- a/docs/notes.txt
+++ b/docs/notes.txt
@@ -176,6 +176,9 @@ oracle -
http://www.notesbit.com/index.php/scripts-oracle/oracle-11g-new-password-algorithm-is-revealed-by-seclistsorg/
http://www.users.zetnet.co.uk/hopwood/crypto/scan/ph.html
+
+lots of sample hashes
+ http://openwall.info/wiki/john/sample-hashes
===========
scrpyt
http://www.tarsnap.com/scrypt.html
diff --git a/passlib/hash/sha1_crypt.py b/passlib/hash/sha1_crypt.py
new file mode 100644
index 0000000..4811517
--- /dev/null
+++ b/passlib/hash/sha1_crypt.py
@@ -0,0 +1,159 @@
+"""passlib.sha1_crypt
+
+Implementation of NetBSD's sha1-based crypt algorithm.
+
+from source -
+http://fxr.googlebit.com/source/lib/libcrypt/crypt-sha1.c?v=NETBSD-CURRENT
+
+thread discussing md5-crypt vs sha1-crypt
+http://osdir.com/ml/os.netbsd.devel.security/2005-06/msg00025.html
+
+when sha1-crypt was added -
+http://mail-index.netbsd.org/tech-userlevel/2004/05/29/0001.html
+"""
+
+#$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a
+#$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH
+
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement, absolute_import
+#core
+from hmac import new as hmac
+from hashlib import sha1
+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
+#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
+ 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)
+
+#=========================================================
+#algorithm information
+#=========================================================
+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
+max_salt_chars = 64
+
+default_rounds = 40000 #current passlib default
+ #todo: vary default value by some % down
+min_rounds = 1 #really, this should be higher.
+max_rounds = 4294967295 # 32-bit integer limit
+rounds_cost = "linear"
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$sha1
+ \$(?P<rounds>\d+)
+ \$(?P<salt>[A-Za-z0-9./]{0,64})
+ (\$(?P<chk>[A-Za-z0-9./]{28}))?
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid sha1_crypt hash"
+ rounds, salt, chk = m.group("rounds", "salt", "chk")
+ return dict(
+ rounds=int(rounds),
+ salt=salt,
+ checksum=chk,
+ )
+
+def render(rounds, salt, checksum=None):
+ out = "$sha1$%d$%s" % (rounds, salt)
+ if checksum:
+ out += "$" + checksum
+ return out
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None):
+ salt = norm_salt(salt, min_salt_chars, max_salt_chars, default_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
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+ info = parse(config)
+ rounds, salt = info['rounds'], info['salt']
+
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ result = salt + "$sha1$" + str(rounds)
+ r = 0
+ while r < rounds:
+ result = hmac_sha1(secret, result)
+ r += 1
+ chk = h64.encode_transposed_bytes(result, _chk_offsets)
+ return render(rounds, salt, chk)
+
+_chk_offsets = [
+ 2,1,0,
+ 5,4,3,
+ 8,7,6,
+ 11,10,9,
+ 14,13,12,
+ 17,16,15,
+ 0,19,18,
+]
+
+#=========================================================
+#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())
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/tests/test_hash_misc.py b/passlib/tests/test_hash_misc.py
index da3d0ba..f99c4e4 100644
--- a/passlib/tests/test_hash_misc.py
+++ b/passlib/tests/test_hash_misc.py
@@ -51,5 +51,23 @@ class NTHashTest(_HandlerTestCase):
)
#=========================================================
+# netbsd sha1 crypt
+#=========================================================
+from passlib.hash import sha1_crypt
+
+class SHA1CryptTest(_HandlerTestcase):
+ handler = sha1_crypt
+
+ known_correct = (
+ ("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"),
+ ("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"),
+ )
+
+ known_invalid = (
+ #bad char in otherwise correct hash
+ '$sha1$21773$u!7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH',
+ )
+
+#=========================================================
#EOF
#=========================================================
diff --git a/passlib/unix.py b/passlib/unix.py
index 8e8beca..32616db 100644
--- a/passlib/unix.py
+++ b/passlib/unix.py
@@ -74,12 +74,12 @@ linux_context = CryptContext([ "unix_disabled", "des_crypt", "md5_crypt", "sha25
#referencing source via -http://fxr.googlebit.com
# freebsd 6,7,8 - des, md5, bcrypt, nthash
-# netbsd - des, ext, md5, bcrypt, sha1 (TODO)
+# netbsd - des, ext, md5, bcrypt, sha1
# openbsd - des, ext, md5, bcrypt
bsd_context = CryptContext(["unix_disabled", "nthash", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"])
freebsd_context = CryptContext([ "unix_disabled", "des_crypt", "nthash", "md5_crypt", "bcrypt"])
openbsd_context = CryptContext([ "unix_disabled", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"])
-netbsd_context = CryptContext([ "unix_disabled", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"])
+netbsd_context = CryptContext([ "unix_disabled", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt", "sha1_crypt"])
#=========================================================
#eof