diff options
author | Donald Stufft <donald@stufft.io> | 2013-02-12 03:36:06 -0500 |
---|---|---|
committer | Donald Stufft <donald@stufft.io> | 2013-02-12 03:36:06 -0500 |
commit | 41362f694af6851b1f3428ba38ebc495e60cad73 (patch) | |
tree | c10119bd7343ff498df9996420c009ade99464de | |
parent | 622d5b0defc2c08e58a5544c0423cc7d98538cf3 (diff) | |
download | decorator-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.template | 2 | ||||
-rw-r--r-- | config.py | 5 | ||||
-rw-r--r-- | legacy_passwords.py | 67 | ||||
-rw-r--r-- | tools/upgradepw.py | 49 |
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 = @@ -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]" |