Merge pull request #1149 from niphlod/enhancement/constant_time_compare

bultin constant time checking
This commit is contained in:
mdipierro
2016-01-04 09:39:38 -06:00
2 changed files with 60 additions and 60 deletions
+51 -59
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']
@@ -265,7 +253,7 @@ class Mail(object):
MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1)) MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1))
self.set_payload(payload) self.set_payload(payload)
self['Content-Disposition'] = 'attachment; filename="%s"' % filename self['Content-Disposition'] = 'attachment; filename="%s"' % filename
if not content_id is None: if content_id is not None:
self['Content-Id'] = '<%s>' % content_id.encode(encoding) self['Content-Id'] = '<%s>' % content_id.encode(encoding)
Encoders.encode_base64(self) Encoders.encode_base64(self)
@@ -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':
@@ -1192,8 +1181,8 @@ class AuthJWT(object):
def login_and_take_token(): def login_and_take_token():
return myjwt.jwt_token_manager() return myjwt.jwt_token_manager()
A call then to /app/controller/login_and_take_token/auth with username and password returns the token A call then to /app/controller/login_and_take_token with username and password returns the token
A call to /app/controller/login_and_take_token/refresh with the original token returns the refreshed token A call to /app/controller/login_and_take_token with the original token returns the refreshed token
To protect a function with JWT To protect a function with JWT
@@ -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))
@@ -1324,7 +1313,10 @@ class AuthJWT(object):
We (mis)use the heavy default auth mechanism to avoid any further computation, We (mis)use the heavy default auth mechanism to avoid any further computation,
while sticking to a somewhat-stable Auth API. while sticking to a somewhat-stable Auth API.
""" """
now = time.mktime(datetime.datetime.utcnow().timetuple()) ## is the following safe or should we use
## calendar.timegm(datetime.datetime.utcnow().timetuple())
## result seem to be the same (seconds since epoch, in UTC)
now = time.mktime(datetime.datetime.now().timetuple())
expires = now + self.expiration expires = now + self.expiration
payload = dict( payload = dict(
hmac_key=session_auth['hmac_key'], hmac_key=session_auth['hmac_key'],
@@ -1336,7 +1328,7 @@ class AuthJWT(object):
return payload return payload
def refresh_token(self, orig_payload): def refresh_token(self, orig_payload):
now = time.mktime(datetime.datetime.utcnow().timetuple()) now = time.mktime(datetime.datetime.now().timetuple())
if self.verify_expiration: if self.verify_expiration:
orig_exp = orig_payload['exp'] orig_exp = orig_payload['exp']
if orig_exp + self.leeway < now: if orig_exp + self.leeway < now:
@@ -1346,7 +1338,7 @@ class AuthJWT(object):
if orig_iat + self.refresh_expiration_delta < now: if orig_iat + self.refresh_expiration_delta < now:
# refreshed too long ago # refreshed too long ago
raise HTTP(400, u'Token issued too long ago') raise HTTP(400, u'Token issued too long ago')
expires = now + self.refresh_expiration_delta expires = now + self.expiration
orig_payload.update( orig_payload.update(
orig_iat=orig_iat, orig_iat=orig_iat,
iat=now, iat=now,
@@ -1372,14 +1364,17 @@ class AuthJWT(object):
def api_auth(): def api_auth():
return myjwt.jwt_token_manager() return myjwt.jwt_token_manager()
Then, a call to /app/c/api_auth/auth with username and password Then, a call to /app/c/api_auth with username and password
returns a token, while /app/c/api_auth/refresh with the current token returns a token, while /app/c/api_auth with the current token
issues another token issues another token
""" """
request = current.request request = current.request
response = current.response response = current.response
session = current.session session = current.session
# forget and unlock response # forget and unlock response
session.forget(response)
valid_user = None
ret = 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 +1382,23 @@ 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)
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
response.headers['content-type'] = 'application/json' if valid_user:
return json.dumps(ret) payload = self.serialize_auth_session(current.session.auth)
self.alter_payload(payload)
ret = {'token': self.generate_token(payload)}
elif ret is None:
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'
return serializers.json(ret)
def inject_token(self, tokend): def inject_token(self, tokend):
""" """
@@ -1846,7 +1840,6 @@ 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 +1924,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 +2949,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 +3752,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 +3851,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),
@@ -4045,18 +4038,18 @@ class Auth(object):
def jwt(self): def jwt(self):
""" """
To use JWT authentication: To use JWT authentication:
1) instantiate auth with 1) instantiate auth with::
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) Decorate 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!
@@ -4074,7 +4067,7 @@ class Auth(object):
Authorization: Bearer <the jwt token> Authorization: Bearer <the jwt token>
Any additional attributes in the jwt argument of Auth() below: Any additional attributes in the jwt argument of Auth() below::
auth = Auth(db, jwt = {...}) auth = Auth(db, jwt = {...})
@@ -4083,9 +4076,9 @@ class Auth(object):
if not self.jwt_handler: if not self.jwt_handler:
raise HTTP(400, "Not authorized") raise HTTP(400, "Not authorized")
else: else:
current.response.headers['content-type'] = 'application/json' rtn = self.jwt_handler.jwt_token_manager()
raise HTTP(200, self.jwt_handler.jwt_token_manager()) raise HTTP(200, rtn, cookies=None, **current.response.headers)
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
@@ -4188,7 +4181,7 @@ class Auth(object):
if not self.jwt_handler: if not self.jwt_handler:
raise HTTP(400, "Not authorized") raise HTTP(400, "Not authorized")
else: else:
return self.jwt_handler.allows_jwt() return self.jwt_handler.allows_jwt(otherwise=otherwise)
def requires(self, condition, requires_login=True, otherwise=None): def requires(self, condition, requires_login=True, otherwise=None):
""" """
@@ -4201,7 +4194,6 @@ class Auth(object):
basic_allowed, basic_accepted, user = self.basic() basic_allowed, basic_accepted, user = self.basic()
user = user or self.user user = user or self.user
login_required = requires_login login_required = requires_login
if callable(login_required): if callable(login_required):
login_required = login_required() login_required = login_required()
@@ -4210,7 +4202,7 @@ class Auth(object):
if not user: if not user:
if current.request.ajax: if current.request.ajax:
raise HTTP(401, self.messages.ajax_failed_authentication) raise HTTP(401, self.messages.ajax_failed_authentication)
elif not otherwise is None: elif otherwise is not None:
if callable(otherwise): if callable(otherwise):
return otherwise() return otherwise()
redirect(otherwise) redirect(otherwise)
@@ -4248,7 +4240,7 @@ class Auth(object):
return self.requires(True, otherwise=otherwise) return self.requires(True, otherwise=otherwise)
def requires_login_or_token(self, otherwise=None): def requires_login_or_token(self, otherwise=None):
if self.settings.enable_tokens == True: if self.settings.enable_tokens is True:
user = None user = None
request = current.request request = current.request
token = request.env.http_web2py_user_token or request.vars._token token = request.env.http_web2py_user_token or request.vars._token
+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()