about summary refs log tree commit diff
path: root/wqflask
diff options
context:
space:
mode:
authorSam2013-09-27 00:24:38 -0500
committerSam2013-09-27 00:24:38 -0500
commitfaaa4f0a7b08d5a07c2a5a403774942e374ce0a3 (patch)
treeb4f827c4c7e1aa1dd972e2316debafe1ba1aadcb /wqflask
parent63c691fb11a33ac70c831f5651d284f38dc27b5b (diff)
downloadgenenetwork2-faaa4f0a7b08d5a07c2a5a403774942e374ce0a3.tar.gz
Before fixing an issue in pbkdf2
Diffstat (limited to 'wqflask')
-rw-r--r--wqflask/wqflask/database.py22
-rw-r--r--wqflask/wqflask/model.py55
-rw-r--r--wqflask/wqflask/pbkdf2.py130
-rw-r--r--wqflask/wqflask/templates/base.html2
-rw-r--r--wqflask/wqflask/templates/new_security/register_user.html6
-rw-r--r--wqflask/wqflask/user_manager.py76
-rw-r--r--wqflask/wqflask/views.py1
7 files changed, 251 insertions, 41 deletions
diff --git a/wqflask/wqflask/database.py b/wqflask/wqflask/database.py
new file mode 100644
index 00000000..d93987bc
--- /dev/null
+++ b/wqflask/wqflask/database.py
@@ -0,0 +1,22 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.ext.declarative import declarative_base
+
+from wqflask import app
+
+#engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
+engine = create_engine(app.config['DB_URI'], convert_unicode=True)
+
+db_session = scoped_session(sessionmaker(autocommit=False,
+                                         autoflush=False,
+                                         bind=engine))
+Base = declarative_base()
+Base.query = db_session.query_property()
+
+def init_db():
+    # import all modules here that might define models so that
+    # they will be registered properly on the metadata.  Otherwise
+    # you will have to import them first before calling init_db()
+    #import yourapplication.models
+    import wqflask.model
+    Base.metadata.create_all(bind=engine)
\ No newline at end of file
diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py
index b3dfe746..603cfbc4 100644
--- a/wqflask/wqflask/model.py
+++ b/wqflask/wqflask/model.py
@@ -1,10 +1,12 @@
 from __future__ import print_function, division, absolute_import
 
+import uuid
+
 from flask.ext.sqlalchemy import SQLAlchemy
-from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin
+#from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin
 
-from flask_security.forms import TextField
-from flask_security.forms import RegisterForm
+#from flask_security.forms import TextField
+#from flask_security.forms import RegisterForm
 
 from wqflask import app
 
@@ -37,18 +39,21 @@ db = SQLAlchemy(app)
 
 # Define models
 roles_users = db.Table('roles_users',
-        db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
-        db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
-
-class Role(db.Model, RoleMixin):
-    id = db.Column(db.Integer(), primary_key=True)
-    name = db.Column(db.String(80), unique=True)
-    description = db.Column(db.String(255))
-
-class User(db.Model, UserMixin):
-    id = db.Column(db.Integer(), primary_key=True)
-    email = db.Column(db.String(50), unique=True)
-    password = db.Column(db.String(50))
+        db.Column('user_id', db.Integer(), db.ForeignKey('user.the_id')),
+        db.Column('role_id', db.Integer(), db.ForeignKey('role.the_id')))
+
+class Role(db.Model):
+    the_id = db.Column(db.Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4()))
+    name = db.Column(db.Unicode(80), unique=True, nullable=False)
+    description = db.Column(db.Unicode(255))
+
+class User(db.Model):
+    the_id = db.Column(db.Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4()))
+    email_address = db.Column(db.Unicode(50), unique=True, nullable=False)
+    
+    password = db.Column(db.Unicode(24), nullable=False)
+    salt = db.Column(db.Unicode(32), nullable=False)
+    password_info = db.Column(db.Unicode(50))
     
     full_name = db.Column(db.Unicode(50))
     organization = db.Column(db.Unicode(50))
@@ -58,23 +63,23 @@ class User(db.Model, UserMixin):
 
     last_login_at = db.Column(db.DateTime())
     current_login_at = db.Column(db.DateTime())
-    last_login_ip = db.Column(db.String(39))
-    current_login_ip = db.Column(db.String(39))
+    last_login_ip = db.Column(db.Unicode(39))
+    current_login_ip = db.Column(db.Unicode(39))
     login_count = db.Column(db.Integer())
 
     roles = db.relationship('Role', secondary=roles_users,
                             backref=db.backref('users', lazy='dynamic'))
 
 # Setup Flask-Security
-user_datastore = SQLAlchemyUserDatastore(db, User, Role)
-
-class ExtendedRegisterForm(RegisterForm):
-    name = TextField('name')
-    #print("name is:", name['_name'], vars(name))
-    organization = TextField('organization')
+#user_datastore = SQLAlchemyUserDatastore(db, User, Role)
 
-security = Security(app, user_datastore, register_form=ExtendedRegisterForm)
+#class ExtendedRegisterForm(RegisterForm):
+#    name = TextField('name')
+#    #print("name is:", name['_name'], vars(name))
+#    organization = TextField('organization')
+#
+#security = Security(app, user_datastore, register_form=ExtendedRegisterForm)
 
 db.metadata.create_all(db.engine)
 
-user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)")
+#user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)")
diff --git a/wqflask/wqflask/pbkdf2.py b/wqflask/wqflask/pbkdf2.py
new file mode 100644
index 00000000..b7a7dd42
--- /dev/null
+++ b/wqflask/wqflask/pbkdf2.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+"""
+    pbkdf2
+    ~~~~~~
+
+    This module implements pbkdf2 for Python.  It also has some basic
+    tests that ensure that it works.  The implementation is straightforward
+    and uses stdlib only stuff and can be easily be copy/pasted into
+    your favourite application.
+
+    Use this as replacement for bcrypt that does not need a c implementation
+    of a modified blowfish crypto algo.
+
+    Example usage:
+
+    >>> pbkdf2_hex('what i want to hash', 'the random salt')
+    'fa7cc8a2b0a932f8e6ea42f9787e9d36e592e0c222ada6a9'
+
+    How to use this:
+
+    1.  Use a constant time string compare function to compare the stored hash
+        with the one you're generating::
+
+            def safe_str_cmp(a, b):
+                if len(a) != len(b):
+                    return False
+                rv = 0
+                for x, y in izip(a, b):
+                    rv |= ord(x) ^ ord(y)
+                return rv == 0
+
+    2.  Use `os.urandom` to generate a proper salt of at least 8 byte.
+        Use a unique salt per hashed password.
+
+    3.  Store ``algorithm$salt:costfactor$hash`` in the database so that
+        you can upgrade later easily to a different algorithm if you need
+        one.  For instance ``PBKDF2-256$thesalt:10000$deadbeef...``.
+
+
+    :copyright: (c) Copyright 2011 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+import hmac
+import hashlib
+from struct import Struct
+from operator import xor
+from itertools import izip, starmap
+
+
+_pack_int = Struct('>I').pack
+
+
+def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None):
+    """Like :func:`pbkdf2_bin` but returns a hex encoded string."""
+    return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).encode('hex')
+
+
+def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None):
+    """Returns a binary digest for the PBKDF2 hash algorithm of `data`
+    with the given `salt`.  It iterates `iterations` time and produces a
+    key of `keylen` bytes.  By default SHA-1 is used as hash function,
+    a different hashlib `hashfunc` can be provided.
+    """
+    hashfunc = hashfunc or hashlib.sha1
+    mac = hmac.new(data, None, hashfunc)
+    def _pseudorandom(x, mac=mac):
+        h = mac.copy()
+        h.update(x)
+        return map(ord, h.digest())
+    buf = []
+    for block in xrange(1, -(-keylen // mac.digest_size) + 1):
+        rv = u = _pseudorandom(salt + _pack_int(block))
+        for i in xrange(iterations - 1):
+            u = _pseudorandom(''.join(map(chr, u)))
+            rv = starmap(xor, izip(rv, u))
+        buf.extend(rv)
+    return ''.join(map(chr, buf))[:keylen]
+
+
+def test():
+    failed = []
+    def check(data, salt, iterations, keylen, expected):
+        rv = pbkdf2_hex(data, salt, iterations, keylen)
+        if rv != expected:
+            print 'Test failed:'
+            print '  Expected:   %s' % expected
+            print '  Got:        %s' % rv
+            print '  Parameters:'
+            print '    data=%s' % data
+            print '    salt=%s' % salt
+            print '    iterations=%d' % iterations
+            print
+            failed.append(1)
+
+    # From RFC 6070
+    check('password', 'salt', 1, 20,
+          '0c60c80f961f0e71f3a9b524af6012062fe037a6')
+    check('password', 'salt', 2, 20,
+          'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957')
+    check('password', 'salt', 4096, 20,
+          '4b007901b765489abead49d926f721d065a429c1')
+    check('passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt',
+          4096, 25, '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038')
+    check('pass\x00word', 'sa\x00lt', 4096, 16,
+          '56fa6aa75548099dcc37d7f03425e0c3')
+    # This one is from the RFC but it just takes for ages
+    ##check('password', 'salt', 16777216, 20,
+    ##      'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984')
+
+    # From Crypt-PBKDF2
+    check('password', 'ATHENA.MIT.EDUraeburn', 1, 16,
+          'cdedb5281bb2f801565a1122b2563515')
+    check('password', 'ATHENA.MIT.EDUraeburn', 1, 32,
+          'cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837')
+    check('password', 'ATHENA.MIT.EDUraeburn', 2, 16,
+          '01dbee7f4a9e243e988b62c73cda935d')
+    check('password', 'ATHENA.MIT.EDUraeburn', 2, 32,
+          '01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86')
+    check('password', 'ATHENA.MIT.EDUraeburn', 1200, 32,
+          '5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13')
+    check('X' * 64, 'pass phrase equals block size', 1200, 32,
+          '139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1')
+    check('X' * 65, 'pass phrase exceeds block size', 1200, 32,
+          '9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a')
+
+    raise SystemExit(bool(failed))
+
+
+if __name__ == '__main__':
+    test()
diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html
index 7f72ff22..6e7119fe 100644
--- a/wqflask/wqflask/templates/base.html
+++ b/wqflask/wqflask/templates/base.html
@@ -70,7 +70,7 @@
                             <a href="/links">Links</a>
                         </li>
                         <li class="">
-                            {% if g.identity.name=="anon" %}
+                            {% if "g.identity.name"=="anon" %}
                             <a id="login_out" class="modalize" href="/n/register">Sign in</a>
                             {% else %}
                             <a id="login_out" href="/n/logout">Sign out</a>
diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html
index 755d0438..2a02e7ca 100644
--- a/wqflask/wqflask/templates/new_security/register_user.html
+++ b/wqflask/wqflask/templates/new_security/register_user.html
@@ -45,7 +45,7 @@
                 <fieldset>
         
                     <div class="control-group">
-                        <label class="control-label" for="email">Email Address</label>
+                        <label class="control-label" for="email_address">Email Address</label>
                         <div class="controls">
                             <input id="email_address" name="email_address" class="focused" type="text" value="{{values.email_address}}"
                                    data-trigger="change" data-required="true" data-type="email" data-maxlength="50">
@@ -53,7 +53,7 @@
                     </div>
                     
                     <div class="control-group">
-                        <label class="control-label" for="email">Full Name</label>
+                        <label class="control-label" for="full_name">Full Name</label>
                         <div class="controls">
                             <input id="full_name" name="full_name" type="text" value="{{values.full_name}}"
                                    data-trigger="change" data-required="true" data-minlength="5" data-maxlength="50">
@@ -61,7 +61,7 @@
                     </div>
         
                     <div class="control-group">
-                        <label class="control-label" for="email">Organization</label>
+                        <label class="control-label" for="organization">Organization</label>
                         <div class="controls">
                             <input id="organization" name="organization" type="text" value="{{values.organization}}" data-minlength="3" data-maxlength="50">
                         </div>
diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py
index c05bb9ce..3f5eee08 100644
--- a/wqflask/wqflask/user_manager.py
+++ b/wqflask/wqflask/user_manager.py
@@ -7,6 +7,17 @@ from __future__ import print_function, division, absolute_import
 
 """
 
+import os
+import hashlib
+import datetime
+import time
+
+import simplejson as json
+
+import pbkdf2
+
+from wqflask.database import db_session
+
 from wqflask import model
 
 from utility import Bunch
@@ -50,32 +61,73 @@ class UserManager(object):
 class RegisterUser(object):
     def __init__(self, kw):
         self.errors = []
-        user = Bunch()
+        self.user = Bunch()
         
-        user.email_address = kw.get('email_address', '').strip()
-        if not (5 <= len(user.email_address) <= 50):
+        self.user.email_address = kw.get('email_address', '').strip()
+        if not (5 <= len(self.user.email_address) <= 50):
             self.errors.append('Email Address needs to be between 5 and 50 characters.')
             
-        user.full_name = kw.get('full_name', '').strip()
-        if not (5 <= len(user.full_name) <= 50):
+        self.user.full_name = kw.get('full_name', '').strip()
+        if not (5 <= len(self.user.full_name) <= 50):
             self.errors.append('Full Name needs to be between 5 and 50 characters.')
             
-        user.organization = kw.get('organization', '').strip()
-        if user.organization and not (5 <= len(user.organization) <= 50):
+        self.user.organization = kw.get('organization', '').strip()
+        if self.user.organization and not (5 <= len(self.user.organization) <= 50):
             self.errors.append('Organization needs to be empty or between 5 and 50 characters.')
 
-        user.password = kw.get('password', '')
-        if not (6 <= len(user.password)):
+        password = str(kw.get('password', ''))
+        if not (6 <= len(password)):
             self.errors.append('Password needs to be at least 6 characters.')
             
-        if kw.get('password_confirm') != user.password:
+        if kw.get('password_confirm') != password:
             self.errors.append("Passwords don't match.")
         
         if self.errors:
-            return 
+            return
+        
+        print("No errors!")
+        
+        self.set_password(password)
+        
+        new_user = model.User(**self.user.__dict__)
+        db_session.add(new_user)
+        db_session.commit()
         
     
-
+    def set_password(self, password):
+        pwfields = Bunch()
+        algo_string = "sha256"
+        algorithm = getattr(hashlib, algo_string)
+        pwfields.algorithm = "pbkdf2-" + algo_string
+        pwfields.salt = os.urandom(32)
+        
+        # lastpass did 100000 iterations in 2011 
+        pwfields.iterations = 100000   
+        pwfields.keylength = 24
+        
+        pwfields.created = datetime.datetime.utcnow()
+        # One more check on password length
+        assert len(password) >= 6, "Password shouldn't be so short here"
+        
+        print("pwfields:", vars(pwfields))
+        print("locals:", locals())
+        start_time = time.time()
+        pwfields.password = pbkdf2.pbkdf2_hex(password, pwfields.salt, pwfields.iterations, pwfields.keylength, algorithm)
+        print("Creating password took:", time.time() - start_time)
+        self.user.password = json.dumps(pwfields.__dict__, sort_keys=True)
+
+
+#def combined_salt(user_salt):
+#    """Combine the master salt with the user salt...we use two seperate salts so that if the database is compromised, the
+#    salts aren't immediately known"""
+#    secret_salt = app.confing['SECRET_SALT']
+#    assert len(user_salt) == 32
+#    assert len(secret_salt) == 32
+#    combined = ""
+#    for x, y in user_salt, secret_salt:
+#        combined = combined + x + y
+#    return combined
+    
 
 
 class GroupsManager(object):
diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py
index 9a0401d6..82d28eab 100644
--- a/wqflask/wqflask/views.py
+++ b/wqflask/wqflask/views.py
@@ -302,6 +302,7 @@ def new_register():
     else:
         params = request.args
     if params:
+        print("Attempting to register the user...")
         result = user_manager.RegisterUser(params)
         errors = result.errors
     return render_template("new_security/register_user.html", values=params, errors=errors)