bultin constant time checking

- if hmac.compare_digest is there, we should use it instead of our own
fallback.
- jwt handler has been updated to use utils.compare (reported in
#web2py-users)
- includes the same mods as https://github.com/web2py/web2py/pull/1146
This commit is contained in:
niphlod
2015-12-30 10:37:14 +01:00
parent 1ca0c9b0c0
commit e94946d3d5
2 changed files with 39 additions and 41 deletions
+30 -40
View File
@@ -34,13 +34,12 @@ import email.utils
import random import random
import hmac import hmac
import hashlib import hashlib
import json
from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset
from gluon.serializers import json_parser from gluon.serializers import json_parser
from gluon.contenttype import contenttype from gluon.contenttype import contenttype
from gluon.storage import Storage, StorageList, Settings, Messages from gluon.storage import Storage, StorageList, Settings, Messages
from gluon.utils import web2py_uuid from gluon.utils import web2py_uuid, compare
from gluon.fileutils import read_file, check_credentials from gluon.fileutils import read_file, check_credentials
from gluon import * from gluon import *
from gluon.contrib.autolinks import expand_one from gluon.contrib.autolinks import expand_one
@@ -53,17 +52,6 @@ import gluon.serializers as serializers
Table = DAL.Table Table = DAL.Table
Field = DAL.Field Field = DAL.Field
try:
# try stdlib (Python 2.6)
import json as json_parser
except ImportError:
try:
# try external module
import simplejson as json_parser
except:
# fallback to pure-Python module
import gluon.contrib.simplejson as json_parser
__all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', __all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki',
'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate']
@@ -1143,8 +1131,7 @@ def addrow(form, a, b, c, style, _id, position=-1):
class AuthJWT(object): class AuthJWT(object):
""" """
If left externally, this needs the usual "singleton" approach. Experimental!
Given I (we) don't know if to include in auth yet, let's stick to basics.
Args: Args:
- secret_key: the secret. Without salting, an attacker knowing this can impersonate - secret_key: the secret. Without salting, an attacker knowing this can impersonate
@@ -1175,7 +1162,9 @@ class AuthJWT(object):
return payload return payload
- before_authorization: can be a callable that takes the deserialized token (a dict) as input. - before_authorization: can be a callable that takes the deserialized token (a dict) as input.
Gets called right after signature verification but before the actual Gets called right after signature verification but before the actual
authorization takes place. You can raise with HTTP a proper error message authorization takes place. It may be use to cast
the extra auth_user fields to their actual types.
You can raise with HTTP a proper error message
Example: Example:
def mybefore_authorization(tokend): def mybefore_authorization(tokend):
if not tokend['my_name_is'] == 'bond,james bond': if not tokend['my_name_is'] == 'bond,james bond':
@@ -1204,7 +1193,7 @@ class AuthJWT(object):
""" """
def __init__(self, def __init__(self,
auth, auth,
secret_key, secret_key,
algorithm='HS256', algorithm='HS256',
@@ -1256,7 +1245,7 @@ class AuthJWT(object):
@staticmethod @staticmethod
def jwt_b64e(string): def jwt_b64e(string):
if isinstance(string, unicode): if isinstance(string, unicode):
string = string.encode('uft-8', 'strict') string = string.encode('utf-8', 'strict')
return base64.urlsafe_b64encode(string).strip(b'=') return base64.urlsafe_b64encode(string).strip(b'=')
@staticmethod @staticmethod
@@ -1279,7 +1268,7 @@ class AuthJWT(object):
if isinstance(secret, unicode): if isinstance(secret, unicode):
secret = secret.encode('ascii', 'ignore') secret = secret.encode('ascii', 'ignore')
b64h = self.cached_b64h b64h = self.cached_b64h
b64p = self.jwt_b64e(json_parser.dumps(payload)) b64p = self.jwt_b64e(serializers.json(payload))
jbody = b64h + '.' + b64p jbody = b64h + '.' + b64p
mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod) mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod)
jsign = self.jwt_b64e(mauth.digest()) jsign = self.jwt_b64e(mauth.digest())
@@ -1287,7 +1276,7 @@ class AuthJWT(object):
def verify_signature(self, body, signature, secret): def verify_signature(self, body, signature, secret):
mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod) mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod)
return hmac.compare_digest(self.jwt_b64e(mauth.digest()), signature) return compare(self.jwt_b64e(mauth.digest()), signature)
def load_token(self, token): def load_token(self, token):
if isinstance(token, unicode): if isinstance(token, unicode):
@@ -1298,7 +1287,7 @@ class AuthJWT(object):
# header not the same # header not the same
raise HTTP(400, u'Invalid JWT Header') raise HTTP(400, u'Invalid JWT Header')
secret = self.secret_key secret = self.secret_key
tokend = json_parser.loads(self.jwt_b64d(b64b)) tokend = serializers.loads_json(self.jwt_b64d(b64b))
if self.salt: if self.salt:
if callable(self.salt): if callable(self.salt):
secret = "%s$%s" % (secret, self.salt(tokend)) secret = "%s$%s" % (secret, self.salt(tokend))
@@ -1380,6 +1369,7 @@ class AuthJWT(object):
response = current.response response = current.response
session = current.session session = current.session
# forget and unlock response # forget and unlock response
valid_user = None
if request.vars.token: if request.vars.token:
if not self.allow_refresh: if not self.allow_refresh:
raise HTTP(403, u'Refreshing token is not allowed') raise HTTP(403, u'Refreshing token is not allowed')
@@ -1387,24 +1377,24 @@ class AuthJWT(object):
tokend = self.load_token(token) tokend = self.load_token(token)
# verification can fail here # verification can fail here
refreshed = self.refresh_token(tokend) refreshed = self.refresh_token(tokend)
ret = {'token':self.generate_token(refreshed)} ret = {'token': self.generate_token(refreshed)}
elif self.user_param in request.vars and self.pass_param in request.vars: elif self.user_param in request.vars and self.pass_param in request.vars:
session.forget(response) session.forget(response)
username = request.vars[self.user_param] username = request.vars[self.user_param]
password = request.vars[self.pass_param] password = request.vars[self.pass_param]
valid_user = self.auth.login_bare(username, password) valid_user = self.auth.login_bare(username, password)
if valid_user:
payload = self.serialize_auth_session(current.session.auth)
self.alter_payload(payload)
ret = {'token':self.generate_token(payload)}
else:
raise HTTP(
401, u'Not Authorized',
**{'WWW-Authenticate': u'JWT realm="%s"' % self.realm})
else: else:
raise HTTP(400, u'Must pass token for refresh or username and password for login') valid_user = self.auth.user
if valid_user:
payload = self.serialize_auth_session(current.session.auth)
self.alter_payload(payload)
ret = {'token': self.generate_token(payload)}
else:
raise HTTP(
401, u'Not Authorized - need to be logged in, to pass a token for refresh or username and password for login',
**{'WWW-Authenticate': u'JWT realm="%s"' % self.realm})
response.headers['content-type'] = 'application/json' response.headers['content-type'] = 'application/json'
return json.dumps(ret) return serializers.json(ret)
def inject_token(self, tokend): def inject_token(self, tokend):
""" """
@@ -1846,7 +1836,7 @@ class Auth(object):
self.define_signature() self.define_signature()
else: else:
self.signature = None self.signature = None
self.jwt_handler = jwt and AuthJWT(self, **jwt) self.jwt_handler = jwt and AuthJWT(self, **jwt)
def get_vars_next(self): def get_vars_next(self):
@@ -1931,7 +1921,7 @@ class Auth(object):
elif args(1) == self.settings.cas_actions['proxyvalidate']: elif args(1) == self.settings.cas_actions['proxyvalidate']:
return self.cas_validate(version=2, proxy=True) return self.cas_validate(version=2, proxy=True)
elif args(1) == self.settings.cas_actions['logout']: elif args(1) == self.settings.cas_actions['logout']:
return self.logout(next=request.vars.service or DEFAULT) return self.logout(next=request.vars.service or DEFAULT)
else: else:
raise HTTP(404) raise HTTP(404)
@@ -2956,7 +2946,7 @@ class Auth(object):
user = table_user(**{username: entered_username}) user = table_user(**{username: entered_username})
if user: if user:
# user in db, check if registration pending or disabled # user in db, check if registration pending or disabled
temp_user = user temp_user = user
if (temp_user.registration_key or '').startswith('pending'): if (temp_user.registration_key or '').startswith('pending'):
response.flash = self.messages.registration_pending response.flash = self.messages.registration_pending
return form return form
@@ -3759,7 +3749,7 @@ class Auth(object):
except Exception: except Exception:
session.flash = self.messages.invalid_reset_password session.flash = self.messages.invalid_reset_password
redirect(next, client_side=self.settings.client_side) redirect(next, client_side=self.settings.client_side)
key = user.registration_key key = user.registration_key
if key in ('pending', 'disabled', 'blocked') or (key or '').startswith('pending'): if key in ('pending', 'disabled', 'blocked') or (key or '').startswith('pending'):
session.flash = self.messages.registration_pending session.flash = self.messages.registration_pending
@@ -3858,7 +3848,7 @@ class Auth(object):
onvalidation=onvalidation, onvalidation=onvalidation,
hideerror=self.settings.hideerror): hideerror=self.settings.hideerror):
user = table_user(**{userfield:form.vars.get(userfield)}) user = table_user(**{userfield:form.vars.get(userfield)})
key = user.registration_key key = user.registration_key
if not user: if not user:
session.flash = self.messages['invalid_%s' % userfield] session.flash = self.messages['invalid_%s' % userfield]
redirect(self.url(args=request.args), redirect(self.url(args=request.args),
@@ -4049,14 +4039,14 @@ class Auth(object):
auth = Auth(db, jwt = {'secret_key':'secret'}) auth = Auth(db, jwt = {'secret_key':'secret'})
where 'secret' is your own secret string. where 'secret' is your own secret string.
2) Secorate functions that require login but should accept the JWT token credentials: 2) Secorate functions that require login but should accept the JWT token credentials:
@auth.allows_jwt() @auth.allows_jwt()
@auth.requires_login() @auth.requires_login()
def myapi(): return 'hello %s' % auth.user.email def myapi(): return 'hello %s' % auth.user.email
Notice jwt is allowed but not required. if user is logged in, myapi is accessible. Notice jwt is allowed but not required. if user is logged in, myapi is accessible.
3) Use it! 3) Use it!
@@ -4085,7 +4075,7 @@ class Auth(object):
else: else:
current.response.headers['content-type'] = 'application/json' current.response.headers['content-type'] = 'application/json'
raise HTTP(200, self.jwt_handler.jwt_token_manager()) raise HTTP(200, self.jwt_handler.jwt_token_manager())
def is_impersonating(self): def is_impersonating(self):
return self.is_logged_in() and 'impersonator' in current.session.auth return self.is_logged_in() and 'impersonator' in current.session.auth
+9 -1
View File
@@ -64,6 +64,10 @@ else:
except (ImportError, ValueError): except (ImportError, ValueError):
HAVE_PBKDF2 = False HAVE_PBKDF2 = False
HAVE_COMPARE_DIGEST = False
if hasattr(hmac, 'compare_digest'):
HAVE_COMPARE_DIGEST = True
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
@@ -77,6 +81,8 @@ def AES_new(key, IV=None):
def compare(a, b): def compare(a, b):
""" Compares two strings and not vulnerable to timing attacks """ """ Compares two strings and not vulnerable to timing attacks """
if HAVE_COMPARE_DIGEST:
return hmac.compare_digest(a, b)
if len(a) != len(b): if len(a) != len(b):
return False return False
result = 0 result = 0
@@ -143,6 +149,7 @@ DIGEST_ALG_BY_SIZE = {
512 / 4: 'sha512', 512 / 4: 'sha512',
} }
def get_callable_argspec(fn): def get_callable_argspec(fn):
if inspect.isfunction(fn) or inspect.ismethod(fn): if inspect.isfunction(fn) or inspect.ismethod(fn):
inspectable = fn inspectable = fn
@@ -154,6 +161,7 @@ def get_callable_argspec(fn):
inspectable = fn inspectable = fn
return inspect.getargspec(inspectable) return inspect.getargspec(inspectable)
def pad(s, n=32, padchar=' '): def pad(s, n=32, padchar=' '):
return s + (32 - len(s) % 32) * padchar return s + (32 - len(s) % 32) * padchar
@@ -172,7 +180,7 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None):
def secure_loads(data, encryption_key, hash_key=None, compression_level=None): def secure_loads(data, encryption_key, hash_key=None, compression_level=None):
if not ':' in data: if ':' not in data:
return None return None
if not hash_key: if not hash_key:
hash_key = sha1(encryption_key).hexdigest() hash_key = sha1(encryption_key).hexdigest()