Auth(...,salt=True), thanks Dave Stoll

This commit is contained in:
mdipierro
2012-07-04 19:22:40 -05:00
parent 3d6335ce7c
commit ae83fe9ac4
7 changed files with 93 additions and 35 deletions
+1 -1
View File
@@ -1 +1 @@
Version 2.00.0 (2012-07-04 17:45:52) dev
Version 2.00.0 (2012-07-04 19:22:37) dev
+1 -1
View File
@@ -50,7 +50,7 @@
</li><li>Corne Dickens (import mechanism)
</li><li>Craig Younkins (Security)
</li><li>Daniel Lin (Taiwanese internationalization)
</li><li>Dave Stoll (DowCommerce payment API)
</li><li>Dave Stoll (DowCommerce payment API, security)
</li><li>David Wagner (security and cryptography expert)
</li><li>Denes Lengyel (validators, DB2 support, DAL, custom forms, legacy table support)
</li><li>Douglas Soares de Andrade (2.4 and 2.6 compliance, docstrings)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
d879ea69-ddb6-4c82-8eab-af68c53de7c4
sha512:9d649235-cf34-4b0c-b103-28e985bc3d40
+1 -1
View File
@@ -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 '<no table>.%s' % quote(self.name)
+27 -25
View File
@@ -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
+61 -5
View File
@@ -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 <algorithm>$<salt>$<hash>
"""
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:
<algorithm>$<salt>$<hash>
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)<self.min_length:
return ('', translate(self.error_message))
if self.key:
return (hmac_hash(value, self.key, self.digest_alg), None)
else:
return (simple_hash(value, self.digest_alg), None)
return (LazyCrypt(self,value),None)
class IS_STRONG(object):