Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'release/0.1'

  • Loading branch information...
commit 8a766eabd568e4d64b908a3cdaf055ec1499e8f1 2 parents 04fd2d4 + 7d4b445
@allanlei authored
View
27 .gitignore
@@ -0,0 +1,27 @@
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist
+*build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
View
10 CHANGES
@@ -0,0 +1,10 @@
+Flask-Passlib Changelog
+====================
+
+
+Version 0.1
+---------------------
+
+Released Jan, 11 2013
+
+ - Initial development
View
9 LICENSE
@@ -0,0 +1,9 @@
+Copyright (c) 2013, Allan Lei <allanlei@helveticode.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+Neither the name of the Helveticode.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
View
0  README.md
No changes.
View
30 flask_passlib/__init__.py
@@ -0,0 +1,30 @@
+from .context import flask_context
+from passlib.context import LazyCryptContext
+
+
+class Passlib(object):
+ def __init__(self, app=None, **kwargs):
+ if app:
+ self.init_app(app, **kwargs)
+
+ def init_app(self, app, context=flask_context, schemes=None):
+ self.app = app
+
+ # register extension with app
+ self.app.extensions = getattr(app, 'extensions', {})
+ self.app.extensions['passlib'] = self
+
+ self.context = context
+
+ def verify(self, *args, **kwargs):
+ return self.context.verify(*args, **kwargs)
+
+ def encrypt(self, *args, **kwargs):
+ if 'salt_length' in kwargs:
+ kwargs['salt_size'] = kwargs.pop('salt_length')
+ if 'method' in kwargs:
+ kwargs['scheme'] = kwargs.pop('method')
+ return self.context.encrypt(*args, **kwargs)
+
+ def identify(self, *args, **kwargs):
+ return self.context.identify(*args, **kwargs)
View
38 flask_passlib/context.py
@@ -0,0 +1,38 @@
+from distutils.version import StrictVersion
+from passlib.context import LazyCryptContext
+import werkzeug
+
+from .handlers import (werkzeug_salted_md5, werkzeug_salted_sha1,
+ werkzeug_salted_sha224, werkzeug_salted_sha256,
+ werkzeug_salted_sha384, werkzeug_salted_sha512)
+
+
+werkzeug061_context = LazyCryptContext(
+ schemes=[
+ werkzeug_salted_md5,
+ werkzeug_salted_sha1,
+ ],
+ default='werkzeug_salted_sha1',
+)
+
+werkzeugdev_context = LazyCryptContext(
+ schemes=[
+ werkzeug_salted_md5,
+ werkzeug_salted_sha1,
+ werkzeug_salted_sha224,
+ werkzeug_salted_sha256,
+ werkzeug_salted_sha384,
+ werkzeug_salted_sha512,
+ ],
+ default='werkzeug_salted_sha1',
+)
+
+
+if StrictVersion('0.6.1') <= StrictVersion(werkzeug.__version__) <= StrictVersion('0.8.3'):
+ werkzeug_context = werkzeug061_context
+elif StrictVersion(werkzeug.__version__) > StrictVersion('0.8.3'):
+ werkzeug_context = werkzeugdev_context
+else:
+ werkzeug_context = werkzeug061_context
+
+flask_context = werkzeug_context
View
108 flask_passlib/handlers.py
@@ -0,0 +1,108 @@
+from werkzeug.security import SALT_CHARS
+
+from passlib.utils.compat import u, str_to_uascii
+from passlib.utils import classproperty
+import passlib.utils.handlers as uh
+
+import hmac
+from hashlib import md5, sha1, sha224, sha256, sha384, sha512
+
+
+class WerkzeugGenericHandler(uh.GenericHandler):
+ checksum_chars = uh.LOWER_HEX_CHARS
+ werkzeug_hash_method = None
+
+ # @classmethod
+ # def identify(cls, hash):
+ # if hash.count('$') < 2:
+ # return False
+ # method, salt, checksum = hash.split('$', 2)
+
+ # # does class specify a known unique prefix to look for?
+ # ident = cls.ident
+ # if ident is not None:
+ # return (method + '$').startswith(ident)
+ # return False
+
+ @classproperty
+ def _stub_checksum(cls):
+ return cls.checksum_chars[0] * cls.checksum_size
+
+class WerkzeugSaltedHash(uh.HasSalt):
+ """base class providing common code for Werkzeug hashes"""
+ # name, ident, checksum_size must be set by subclass.
+ # ident must include "$" suffix.
+ setting_kwds = ("salt", "salt_size")
+
+ min_salt_size = 0
+ default_salt_size = 8
+ max_salt_size = None
+ salt_chars = SALT_CHARS
+
+ def _calc_checksum(self, secret, digestmod=None):
+ if isinstance(secret, unicode):
+ secret = secret.encode('utf-8')
+ if isinstance(self.salt, unicode):
+ self.salt = self.salt.encode('utf-8')
+ # return hmac.new(self.salt, secret, digestmod).hexdigest()
+ return str_to_uascii(hmac.new(self.salt, secret, digestmod).hexdigest())
+
+ @classmethod
+ def from_string(cls, hash):
+ salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
+ return cls(salt=salt, checksum=chk)
+
+ def to_string(self):
+ return uh.render_mc2(self.ident, self.salt,
+ self.checksum or self._stub_checksum)
+
+
+
+
+class werkzeug_salted_md5(WerkzeugSaltedHash, WerkzeugGenericHandler):
+ name = "werkzeug_salted_md5"
+ ident = u("md5$")
+ checksum_size = 32
+
+ def _calc_checksum(self, secret, digestmod=md5):
+ return super(werkzeug_salted_md5, self)._calc_checksum(secret, digestmod=digestmod)
+
+class werkzeug_salted_sha1(WerkzeugSaltedHash, WerkzeugGenericHandler):
+ name = "werkzeug_salted_sha1"
+ ident = u("sha1$")
+ checksum_size = 40
+
+ def _calc_checksum(self, secret, digestmod=sha1):
+ return super(werkzeug_salted_sha1, self)._calc_checksum(secret, digestmod=digestmod)
+
+class werkzeug_salted_sha224(WerkzeugSaltedHash, WerkzeugGenericHandler):
+ name = "werkzeug_salted_sha224"
+ ident = u("sha224$")
+ checksum_size = 56
+
+ def _calc_checksum(self, secret, digestmod=sha224):
+ return super(werkzeug_salted_sha224, self)._calc_checksum(secret, digestmod=digestmod)
+
+class werkzeug_salted_sha256(WerkzeugSaltedHash, WerkzeugGenericHandler):
+ name = "werkzeug_salted_sha256"
+ ident = u("sha256$")
+ checksum_size = 64
+
+ def _calc_checksum(self, secret, digestmod=sha256):
+ return super(werkzeug_salted_sha256, self)._calc_checksum(secret, digestmod=digestmod)
+
+class werkzeug_salted_sha384(WerkzeugSaltedHash, WerkzeugGenericHandler):
+ name = "werkzeug_salted_sha384"
+ ident = u("sha384$")
+ checksum_size = 96
+
+ def _calc_checksum(self, secret, digestmod=sha384):
+ return super(werkzeug_salted_sha384, self)._calc_checksum(secret, digestmod=digestmod)
+
+class werkzeug_salted_sha512(WerkzeugSaltedHash, WerkzeugGenericHandler):
+ name = "werkzeug_salted_sha512"
+ ident = u("sha512$")
+ checksum_size = 128
+
+ def _calc_checksum(self, secret, digestmod=sha512):
+ return super(werkzeug_salted_sha512, self)._calc_checksum(secret, digestmod=digestmod)
View
143 flask_passlib/tests.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+import unittest
+import random
+import string
+
+from flask import Flask
+from flask.ext.passlib import Passlib
+
+from werkzeug.security import generate_password_hash, check_password_hash
+
+WERKZEUG_METHODS = ['md5', 'sha1']
+PASSLIB_METHODS = ['werkzeug_salted_md5', 'werkzeug_salted_sha1']
+
+class FlaskTestCase(unittest.TestCase):
+ TESTING = True
+
+ def setUp(self):
+ self.app = Flask(__name__)
+ self.app.config.from_object(self)
+
+ self.assertTrue(self.app.testing)
+
+ self.passlib = Passlib(self.app)
+
+ self.ctx = self.app.test_request_context()
+ self.ctx.push()
+
+ def tearDown(self):
+ self.ctx.pop()
+
+ def generate_password(self, length=6):
+ return ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(length))
+
+class TestCase(object):
+ ident = None
+ werkzeug_method = None
+ passlib_method = None
+ salt_length = 8
+ checksum_length = 0
+ seperator = '$'
+
+ def test_identify(self):
+ password = self.generate_password()
+
+ # Identify hashes generated by werkzeug
+ passlib_hash = generate_password_hash(password, method=self.werkzeug_method)
+ self.assertEqual(self.passlib.identify(passlib_hash), self.passlib_method)
+
+ # Identify hashes generated by other werkzeug methods
+ diff_werkzeug_method = random.choice([method for method in WERKZEUG_METHODS if method not in [self.werkzeug_method]])
+ passlib_hash = generate_password_hash(password, method=diff_werkzeug_method)
+ self.assertNotEqual(self.passlib.identify(passlib_hash), self.passlib_method)
+
+ # Identify hashes generated by werkzeug with varying salt length
+ passlib_hash = generate_password_hash(password, method=self.werkzeug_method, salt_length=self.salt_length + 1)
+ self.assertEqual(self.passlib.identify(passlib_hash), self.passlib_method)
+
+ # Identify hashes generated by itself
+ passlib_hash = self.passlib.encrypt(password, method=self.passlib_method)
+ self.assertEqual(self.passlib.identify(passlib_hash), self.passlib_method)
+
+ # Identify hashes generated by other methods
+ diff_passlib_method = random.choice([method for method in PASSLIB_METHODS if method not in [self.passlib_method]])
+ passlib_hash = self.passlib.encrypt(password, method=diff_passlib_method)
+ self.assertNotEqual(self.passlib.identify(passlib_hash), self.passlib_method)
+
+ # Identify hashes generated by itself with varying salt length
+ passlib_hash = self.passlib.encrypt(password, method=self.passlib_method, salt_length=self.salt_length + 1)
+ self.assertEqual(self.passlib.identify(passlib_hash), self.passlib_method)
+
+ def test_verify(self):
+ password = self.generate_password()
+ werkzeug_hash = generate_password_hash(password, method=self.werkzeug_method)
+ passlib_hash = self.passlib.encrypt(password, scheme=self.passlib_method)
+
+ self.assertTrue(self.passlib.verify(password, werkzeug_hash))
+ self.assertTrue(self.passlib.verify(password, passlib_hash))
+
+
+ password2 = self.generate_password()
+ werkzeug_hash2 = generate_password_hash(password2, method=self.werkzeug_method)
+ passlib_hash2 = self.passlib.encrypt(password2, scheme=self.passlib_method)
+
+ self.assertTrue(self.passlib.verify(password2, werkzeug_hash2))
+ self.assertTrue(self.passlib.verify(password2, passlib_hash2))
+
+ self.assertFalse(self.passlib.verify(password, werkzeug_hash2))
+ self.assertFalse(self.passlib.verify(password, passlib_hash2))
+ self.assertFalse(self.passlib.verify(password2, werkzeug_hash))
+ self.assertFalse(self.passlib.verify(password2, passlib_hash))
+
+ def test_encrypt(self):
+ password = self.generate_password()
+
+ passlib_hash = self.passlib.encrypt(password, scheme=self.passlib_method, salt_length=self.salt_length)
+ self.assertTrue(check_password_hash(passlib_hash, password))
+ self.assertIn(self.ident, passlib_hash)
+ self.assertTrue(len(passlib_hash.split(self.seperator)), 3)
+ self.assertTrue(passlib_hash.startswith(self.ident))
+ self.assertTrue(len(passlib_hash.split(self.seperator)[1]), self.salt_length)
+ self.assertTrue(len(passlib_hash.split(self.seperator)[2]), self.checksum_length)
+
+
+class MD5TestCase(TestCase, FlaskTestCase):
+ ident = 'md5$'
+ werkzeug_method = 'md5'
+ passlib_method = 'werkzeug_salted_md5'
+ checksum_length = 32
+
+class SHA1TestCase(TestCase, FlaskTestCase):
+ ident = 'sha1$'
+ werkzeug_method = 'sha1'
+ passlib_method = 'werkzeug_salted_sha1'
+ checksum_length = 40
+
+class SHA224TestCase(TestCase, FlaskTestCase):
+ ident = 'sha224$'
+ werkzeug_method = 'sha224'
+ passlib_method = 'werkzeug_salted_sha224'
+ checksum_length = 56
+
+class SHA256TestCase(TestCase, FlaskTestCase):
+ ident = 'sha256$'
+ werkzeug_method = 'sha256'
+ passlib_method = 'werkzeug_salted_sha256'
+ checksum_length = 64
+
+class SHA384TestCase(TestCase, FlaskTestCase):
+ ident = 'sha384$'
+ werkzeug_method = 'sha384'
+ passlib_method = 'werkzeug_salted_sha384'
+ checksum_length = 96
+
+class SHA512TestCase(TestCase, FlaskTestCase):
+ ident = 'sha512$'
+ werkzeug_method = 'sha512'
+ passlib_method = 'werkzeug_salted_sha512'
+ checksum_length = 128
+
+
+
+if __name__ == '__main__':
+ unittest.main()
View
44 setup.py
@@ -0,0 +1,44 @@
+"""
+Flask-Passlib
+----------
+
+Please refer to the online documentation for details.
+
+Links
+`````
+
+* `documentation <https://github.com/allanlei/flask-passlib>`_
+"""
+from setuptools import setup
+
+
+setup(
+ name='Flask-Passlib',
+ version='0.1',
+ url='https://github.com/allanlei/flask-passlib',
+ license='BSD',
+ author='Allan Lei',
+ author_email='allanlei@helveticode.com',
+ description='Flask extension for passlib',
+ long_description=__doc__,
+ py_modules=[
+ 'flask_passlib',
+ ],
+ zip_safe=False,
+ platforms='any',
+ install_requires=[
+ 'Flask',
+ 'passlib==1.6.1',
+ 'Werkzeug>=0.6.1',
+ ],
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ]
+)
Please sign in to comment.
Something went wrong with that request. Please try again.