diff options
Diffstat (limited to 'wqflask')
-rw-r--r-- | wqflask/wqflask/database.py | 22 | ||||
-rw-r--r-- | wqflask/wqflask/model.py | 55 | ||||
-rw-r--r-- | wqflask/wqflask/pbkdf2.py | 130 | ||||
-rw-r--r-- | wqflask/wqflask/templates/base.html | 2 | ||||
-rw-r--r-- | wqflask/wqflask/templates/new_security/register_user.html | 6 | ||||
-rw-r--r-- | wqflask/wqflask/user_manager.py | 76 | ||||
-rw-r--r-- | wqflask/wqflask/views.py | 1 |
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) |