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
+21 -31
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':
@@ -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):
""" """
+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()