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)