From ae83fe9ac42dbff9cea252954420221d75823221 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 4 Jul 2012 19:22:40 -0500 Subject: [PATCH] Auth(...,salt=True), thanks Dave Stoll --- VERSION | 2 +- applications/examples/views/default/who.html | 2 +- applications/welcome/models/db.py | 2 +- applications/welcome/private/auth.key | 2 +- gluon/dal.py | 2 +- gluon/tools.py | 52 +++++++-------- gluon/validators.py | 66 ++++++++++++++++++-- 7 files changed, 93 insertions(+), 35 deletions(-) diff --git a/VERSION b/VERSION index 31c18822..9793a60d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-04 17:45:52) dev +Version 2.00.0 (2012-07-04 19:22:37) dev diff --git a/applications/examples/views/default/who.html b/applications/examples/views/default/who.html index 9844856d..278ad5f5 100644 --- a/applications/examples/views/default/who.html +++ b/applications/examples/views/default/who.html @@ -50,7 +50,7 @@
  • Corne Dickens (import mechanism)
  • Craig Younkins (Security)
  • Daniel Lin (Taiwanese internationalization) -
  • Dave Stoll (DowCommerce payment API) +
  • Dave Stoll (DowCommerce payment API, security)
  • David Wagner (security and cryptography expert)
  • Denes Lengyel (validators, DB2 support, DAL, custom forms, legacy table support)
  • Douglas Soares de Andrade (2.4 and 2.6 compliance, docstrings) diff --git a/applications/welcome/models/db.py b/applications/welcome/models/db.py index 0fc998d0..980084c9 100644 --- a/applications/welcome/models/db.py +++ b/applications/welcome/models/db.py @@ -40,7 +40,7 @@ response.generic_patterns = ['*'] if request.is_local else [] ######################################################################### from gluon.tools import Auth, Crud, Service, PluginManager, prettydate -auth = Auth(db, hmac_key=Auth.get_or_create_key()) +auth = Auth(db, hmac_key=Auth.get_or_create_key(), salt=True) crud, service, plugins = Crud(db), Service(), PluginManager() ## create all tables needed by auth if not custom tables diff --git a/applications/welcome/private/auth.key b/applications/welcome/private/auth.key index 2ab5b736..2a518454 100644 --- a/applications/welcome/private/auth.key +++ b/applications/welcome/private/auth.key @@ -1 +1 @@ -d879ea69-ddb6-4c82-8eab-af68c53de7c4 \ No newline at end of file +sha512:9d649235-cf34-4b0c-b103-28e985bc3d40 \ No newline at end of file diff --git a/gluon/dal.py b/gluon/dal.py index ac50cfae..8a42aa08 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -8152,7 +8152,7 @@ class Field(Expression): def __str__(self): quote = self.db._adapter.varquote try: - return '%s.%s' % (varquote(self.tablename), quote(self.name)) + return '%s.%s' % (quote(self.tablename), quote(self.name)) except: return '.%s' % quote(self.name) diff --git a/gluon/tools.py b/gluon/tools.py index b9cebe12..4badf487 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -880,7 +880,7 @@ class Auth(object): def here(self): return URL(args=current.request.args,vars=current.request.vars) - def __init__(self, environment=None, db=None, mailer=True, + def __init__(self, environment=None, db=None, mailer=True, salt = False, hmac_key=None, controller='default', function='user', cas_provider=None): """ auth=Auth(db) @@ -922,6 +922,7 @@ class Auth(object): settings.hideerror = False settings.password_min_length = 4 + settings.salt = salt settings.cas_domains = [request.env.http_host] settings.cas_provider = cas_provider settings.cas_actions = {'login':'login', @@ -1367,7 +1368,7 @@ class Auth(object): table.username.requires = \ [IS_MATCH('[\w\.\-]+'), IS_NOT_IN_DB(db, table.username)] - if not self.settings.username_case_sensitive: + if not settings.username_case_sensitive: table.username.requires.insert(1,IS_LOWER()) else: table = db.define_table( @@ -1400,12 +1401,12 @@ class Auth(object): table.last_name.requires = \ IS_NOT_EMPTY(error_message=self.messages.is_empty) table[passfield].requires = [ - CRYPT(key=settings.hmac_key, - min_length=self.settings.password_min_length)] + CRYPT(key=settings.hmac_key,salt=settings.salt, + min_length=settings.password_min_length)] table.email.requires = \ [IS_EMAIL(error_message=self.messages.invalid_email), IS_NOT_IN_DB(db, table.email)] - if not self.settings.email_case_sensitive: + if not settings.email_case_sensitive: table.email.requires.insert(1,IS_LOWER()) table.registration_key.default = '' settings.table_user = db[settings.table_user_name] @@ -1523,16 +1524,16 @@ class Auth(object): settings.actions_disabled = \ ['profile','register','change_password','request_reset_password'] from gluon.contrib.login_methods.cas_auth import CasAuth - maps = self.settings.cas_maps + maps = settings.cas_maps if not maps: maps = dict((name,lambda v,n=name:v.get(n,None)) for name in \ settings.table_user.fields if name!='id' \ and settings.table_user[name].readable) maps['registration_id'] = \ lambda v,p=settings.cas_provider:'%s/%s' % (p,v['user']) - actions = [self.settings.cas_actions['login'], - self.settings.cas_actions['servicevalidate'], - self.settings.cas_actions['logout']] + actions = [settings.cas_actions['login'], + settings.cas_actions['servicevalidate'], + settings.cas_actions['logout']] settings.login_form = CasAuth( casversion = 2, urlbase = settings.cas_provider, @@ -1615,7 +1616,7 @@ class Auth(object): user = self.db(table_user[userfield] == username).select().first() if user: password = table_user[passfield].validate(password)[0] - if not user.registration_key and user[passfield] == password: + if not user.registration_key and password == user[passfield]: user = Storage(table_user._filter_fields(user, id=True)) session.auth = Storage(user=user, last_visit=request.now, expiration=self.settings.expiration, @@ -1855,7 +1856,7 @@ class Auth(object): # alternates have failed, maybe because service inaccessible if self.settings.login_methods[0] == self: # try logging in locally using cached credentials - if temp_user[passfield] == form.vars.get(passfield, ''): + if form.vars.get(passfield, '') == temp_user[passfield]: # success user = temp_user else: @@ -2262,7 +2263,7 @@ class Auth(object): redirect(self.url(args=request.args)) password = self.random_password() passfield = self.settings.password_field - d = {passfield: table_user[passfield].validate(password)[0], + d = {passfield: str(table_user[passfield].validate(password)[0]), 'registration_key': ''} user.update_record(**d) if self.settings.mailer and \ @@ -2329,7 +2330,7 @@ class Auth(object): separator=self.settings.label_separator ) if form.accepts(request,session,hideerror=self.settings.hideerror): - user.update_record(**{passfield:form.vars.new_password, + user.update_record(**{passfield:str(form.vars.new_password), 'registration_key':'', 'reset_password_key':''}) session.flash = self.messages.password_changed @@ -2466,10 +2467,7 @@ class Auth(object): form = SQLFORM.factory( Field('old_password', 'password', label=self.messages.old_password, - requires=validators( - table_user[passfield].requires, - IS_IN_DB(s, '%s.%s' % (usern, passfield), - error_message=self.messages.invalid_password))), + requires=table_user[passfield].requires), Field('new_password', 'password', label=self.messages.new_password, requires=table_user[passfield].requires), @@ -2486,15 +2484,19 @@ class Auth(object): formname='change_password', onvalidation=onvalidation, hideerror=self.settings.hideerror): - d = {passfield: form.vars.new_password} - s.update(**d) - session.flash = self.messages.password_changed - self.log_event(log, self.user) - callback(onaccept,form) - if not next: - next = self.url(args=request.args) + + if not form.vars['old_password'] == s.select().first()[passfield]: + form.errors['old_password'] = self.messages.invalid_password else: - next = replace_id(next, form) + d = {passfield: str(form.vars.new_password)} + s.update(**d) + session.flash = self.messages.password_changed + self.log_event(log, self.user) + callback(onaccept,form) + if not next: + next = self.url(args=request.args) + else: + next = replace_id(next, form) redirect(next) return form diff --git a/gluon/validators.py b/gluon/validators.py index 696b1edc..10f47e97 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2519,6 +2519,49 @@ class CLEANUP(Validator): v = self.regex.sub('',str(value).strip()) return (v, None) +class LazyCrypt(object): + """ + Stores a lazy password hash + """ + def __init__(self,crypt,password): + """ + crypt is an instance of the CRYPT validator, + password is the password as inserted by the user + """ + self.crypt = crypt + self.password = password + self.crypted = None + def __str__(self): + """ + Encrypted self.password and caches it in self.crypted. + If self.crypt.salt the output is in the format $$ + """ + if self.crypted: + return self.crypted + if self.crypt.salt: + if self.crypt.salt == True: + salt = str(web2py_uuid()).replace('-','')[-16:] + else: + salt = self.crypt.salt + self.crypted = '%s$%s$%s' % \ + (self.crypt.digest_alg, salt, + simple_hash(self.password+salt, self.crypt.digest_alg)) + elif self.crypt.key: + self.crypted = hmac_hash(self.password, self.crypt.key, self.crypt.digest_alg) + else: + self.crypted = simple_hash(self.password, self.crypt.digest_alg) + return self.crypted + def __eq__(self, stored_password): + """ + compares the current lazy crypted password with a stored password + """ + if self.crypt.salt and stored_password.count('$')==2: + (algorithm, salt, hash) = stored_password.split('$') + temp_pass = '%s$%s$%s' % (algorithm, salt, simple_hash(self.password+salt, algorithm)) + else: + temp_pass = str(self) + return temp_pass == stored_password + class CRYPT(object): """ @@ -2539,21 +2582,34 @@ class CRYPT(object): Notice that an empty password is accepted but invalid. It will not allow login back. Stores junk as hashed password. + + Specify an algorithm or by default we will use sha512. + + Typical available algorithms: + md5, sha1, sha224, sha256, sha384, sha512 + + If salt, it hashes a password with a salt. + If salt is True, this method will automatically generate one. + Either case it returns an encrypted password string in the following format: + + $$ + + Important: hashed password is returned as a LazyCrypt object and computed only if needed. + The LasyCrypt object also knows how to compare itself with an existing salted password + """ - def __init__(self, key=None, digest_alg='md5', min_length=0, error_message='too short'): + def __init__(self, key=None, digest_alg='md5', min_length=0, error_message='too short', salt=None): self.key = key self.digest_alg = digest_alg self.min_length = min_length self.error_message = error_message + self.salt = salt def __call__(self, value): if len(value)