summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDonald Stufft <donald@stufft.io>2013-02-12 03:36:06 -0500
committerDonald Stufft <donald@stufft.io>2013-02-12 03:36:06 -0500
commit41362f694af6851b1f3428ba38ebc495e60cad73 (patch)
treec10119bd7343ff498df9996420c009ade99464de
parent622d5b0defc2c08e58a5544c0423cc7d98538cf3 (diff)
downloaddecorator-41362f694af6851b1f3428ba38ebc495e60cad73.tar.gz
Include a migration path for moving legacy users to a stronger hash
* Includes a method for hashing the sha1 passwords with bcrypt to increase their security * bcrypt_sha1 will upgrade to standard bcrypt as per usual with passlib * Provides a script that migrates 20 users at a time to bcrypt_sha1 Migration script was modified from one written by Giovanni Bajo
-rw-r--r--config.ini.template2
-rw-r--r--config.py5
-rw-r--r--legacy_passwords.py67
-rw-r--r--tools/upgradepw.py49
4 files changed, 122 insertions, 1 deletions
diff --git a/config.ini.template b/config.ini.template
index 93d42ed..44189f2 100644
--- a/config.ini.template
+++ b/config.ini.template
@@ -27,7 +27,7 @@ simple_sign_script = /serversig
[passlib]
; The first listed schemed will automatically be the default, see passlib
; documentation for a full list of options.
-schemes = bcrypt, hex_sha1
+schemes = bcrypt, bcrypt_sha1, hex_sha1
[logging]
file =
diff --git a/config.py b/config.py
index d8489f1..324600e 100644
--- a/config.py
+++ b/config.py
@@ -2,6 +2,11 @@ import ConfigParser
from urlparse import urlsplit, urlunsplit
from passlib.context import CryptContext
+from passlib.registry import register_crypt_handler_path
+
+
+# Register our legacy password handler
+register_crypt_handler_path("bcrypt_sha1", "legacy_passwords")
class Config:
diff --git a/legacy_passwords.py b/legacy_passwords.py
new file mode 100644
index 0000000..1191488
--- /dev/null
+++ b/legacy_passwords.py
@@ -0,0 +1,67 @@
+import base64
+import hashlib
+
+import passlib.exc as exc
+import passlib.utils.handlers as uh
+
+from passlib.registry import get_crypt_handler
+from passlib.utils import to_unicode
+from passlib.utils.compat import uascii_to_str
+
+
+passlib_bcrypt = get_crypt_handler("bcrypt")
+
+
+class bcrypt_sha1(uh.StaticHandler):
+
+ name = "bcrypt_sha1"
+ _hash_prefix = u"$bcrypt_sha1$"
+
+ def _calc_checksum(self, secret):
+ # Hash the secret with sha1 first
+ secret = hashlib.sha1(secret).hexdigest()
+
+ # Hash it with bcrypt
+ return passlib_bcrypt.encrypt(secret)
+
+ def to_string(self):
+ assert self.checksum is not None
+ return uascii_to_str(self._hash_prefix + base64.b64encode(self.checksum))
+
+ @classmethod
+ def from_string(cls, hash, **context):
+ # default from_string() which strips optional prefix,
+ # and passes rest unchanged as checksum value.
+ hash = to_unicode(hash, "ascii", "hash")
+ hash = cls._norm_hash(hash)
+ # could enable this for extra strictness
+ ##pat = cls._hash_regex
+ ##if pat and pat.match(hash) is None:
+ ## raise ValueError("not a valid %s hash" % (cls.name,))
+ prefix = cls._hash_prefix
+ if prefix:
+ if hash.startswith(prefix):
+ hash = hash[len(prefix):]
+ else:
+ raise exc.InvalidHashError(cls)
+
+ # Decode the base64 stored actual hash
+ hash = unicode(base64.b64decode(hash))
+
+ return cls(checksum=hash, **context)
+
+ @classmethod
+ def verify(cls, secret, hash, **context):
+ # NOTE: classes with multiple checksum encodings should either
+ # override this method, or ensure that from_string() / _norm_checksum()
+ # ensures .checksum always uses a single canonical representation.
+ uh.validate_secret(secret)
+ self = cls.from_string(hash, **context)
+ chk = self.checksum
+ if chk is None:
+ raise exc.MissingDigestError(cls)
+
+ # Actually use the verify from passlib_bcrypt after hashing the secret
+ # with sha1
+ secret = hashlib.sha1(secret).hexdigest()
+ return passlib_bcrypt.verify(secret, chk)
diff --git a/tools/upgradepw.py b/tools/upgradepw.py
new file mode 100644
index 0000000..cbdf4fd
--- /dev/null
+++ b/tools/upgradepw.py
@@ -0,0 +1,49 @@
+#!/usr/bin/python
+import base64
+import os
+import sys
+
+# Workaround current bug in docutils:
+# http://permalink.gmane.org/gmane.text.docutils.devel/6324
+import docutils.utils
+
+
+root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.append(root)
+
+import config
+import store
+import passlib.registry
+
+bcrypt = passlib.registry.get_crypt_handler("bcrypt")
+bcrypt_sha1 = passlib.registry.get_crypt_handler("bcrypt_sha1")
+
+cfg = config.Config(os.path.join(root, "config.ini"))
+st = store.Store(cfg)
+
+print "Migrating passwords to bcrypt_sha1 from unsalted sha1....",
+
+st.open()
+for i, u in enumerate(st.get_users()):
+ user = st.get_user(u['name'])
+ # basic sanity check to allow it to run concurrent with users accessing
+ if len(user['password']) == 40 and "$" not in user["password"]:
+ # Hash the existing sha1 password with bcrypt
+ bcrypted = bcrypt.encrypt(user["password"])
+
+ # Base64 encode the bcrypted password so that it's just a blob of data
+ encoded = base64.b64encode(bcrypted)
+
+ st.setpasswd(user['name'], bcrypt_sha1._hash_prefix + encoded,
+ hashed=True,
+ )
+
+ # Commit every 20 users
+ if not i % 20:
+ st.commit()
+ st.open()
+
+st.commit()
+st.close()
+
+print "[ok]"