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

View File

@@ -34,13 +34,12 @@ import email.utils
import random
import hmac
import hashlib
import json
from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset
from gluon.serializers import json_parser
from gluon.contenttype import contenttype
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 import *
from gluon.contrib.autolinks import expand_one
@@ -53,17 +52,6 @@ import gluon.serializers as serializers
Table = DAL.Table
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',
'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate']
@@ -1143,8 +1131,7 @@ def addrow(form, a, b, c, style, _id, position=-1):
class AuthJWT(object):
"""
If left externally, this needs the usual "singleton" approach.
Given I (we) don't know if to include in auth yet, let's stick to basics.
Experimental!
Args:
- secret_key: the secret. Without salting, an attacker knowing this can impersonate
@@ -1175,7 +1162,9 @@ class AuthJWT(object):
return payload
- 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
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:
def mybefore_authorization(tokend):
if not tokend['my_name_is'] == 'bond,james bond':
@@ -1204,7 +1193,7 @@ class AuthJWT(object):
"""
def __init__(self,
def __init__(self,
auth,
secret_key,
algorithm='HS256',
@@ -1256,7 +1245,7 @@ class AuthJWT(object):
@staticmethod
def jwt_b64e(string):
if isinstance(string, unicode):
string = string.encode('uft-8', 'strict')
string = string.encode('utf-8', 'strict')
return base64.urlsafe_b64encode(string).strip(b'=')
@staticmethod
@@ -1279,7 +1268,7 @@ class AuthJWT(object):
if isinstance(secret, unicode):
secret = secret.encode('ascii', 'ignore')
b64h = self.cached_b64h
b64p = self.jwt_b64e(json_parser.dumps(payload))
b64p = self.jwt_b64e(serializers.json(payload))
jbody = b64h + '.' + b64p
mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod)
jsign = self.jwt_b64e(mauth.digest())
@@ -1287,7 +1276,7 @@ class AuthJWT(object):
def verify_signature(self, body, signature, secret):
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):
if isinstance(token, unicode):
@@ -1298,7 +1287,7 @@ class AuthJWT(object):
# header not the same
raise HTTP(400, u'Invalid JWT Header')
secret = self.secret_key
tokend = json_parser.loads(self.jwt_b64d(b64b))
tokend = serializers.loads_json(self.jwt_b64d(b64b))
if self.salt:
if callable(self.salt):
secret = "%s$%s" % (secret, self.salt(tokend))
@@ -1380,6 +1369,7 @@ class AuthJWT(object):
response = current.response
session = current.session
# forget and unlock response
valid_user = None
if request.vars.token:
if not self.allow_refresh:
raise HTTP(403, u'Refreshing token is not allowed')
@@ -1387,24 +1377,24 @@ class AuthJWT(object):
tokend = self.load_token(token)
# verification can fail here
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:
session.forget(response)
username = request.vars[self.user_param]
password = request.vars[self.pass_param]
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:
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'
return json.dumps(ret)
return serializers.json(ret)
def inject_token(self, tokend):
"""
@@ -1846,7 +1836,7 @@ class Auth(object):
self.define_signature()
else:
self.signature = None
self.jwt_handler = jwt and AuthJWT(self, **jwt)
def get_vars_next(self):
@@ -1931,7 +1921,7 @@ class Auth(object):
elif args(1) == self.settings.cas_actions['proxyvalidate']:
return self.cas_validate(version=2, proxy=True)
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:
raise HTTP(404)
@@ -2956,7 +2946,7 @@ class Auth(object):
user = table_user(**{username: entered_username})
if user:
# user in db, check if registration pending or disabled
temp_user = user
temp_user = user
if (temp_user.registration_key or '').startswith('pending'):
response.flash = self.messages.registration_pending
return form
@@ -3759,7 +3749,7 @@ class Auth(object):
except Exception:
session.flash = self.messages.invalid_reset_password
redirect(next, client_side=self.settings.client_side)
key = user.registration_key
if key in ('pending', 'disabled', 'blocked') or (key or '').startswith('pending'):
session.flash = self.messages.registration_pending
@@ -3858,7 +3848,7 @@ class Auth(object):
onvalidation=onvalidation,
hideerror=self.settings.hideerror):
user = table_user(**{userfield:form.vars.get(userfield)})
key = user.registration_key
key = user.registration_key
if not user:
session.flash = self.messages['invalid_%s' % userfield]
redirect(self.url(args=request.args),
@@ -4049,14 +4039,14 @@ class Auth(object):
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:
@auth.allows_jwt()
@auth.requires_login()
def myapi(): return 'hello %s' % auth.user.email
Notice jwt is allowed but not required. if user is logged in, myapi is accessible.
3) Use it!
@@ -4085,7 +4075,7 @@ class Auth(object):
else:
current.response.headers['content-type'] = 'application/json'
raise HTTP(200, self.jwt_handler.jwt_token_manager())
def is_impersonating(self):
return self.is_logged_in() and 'impersonator' in current.session.auth

View File

@@ -64,6 +64,10 @@ else:
except (ImportError, ValueError):
HAVE_PBKDF2 = False
HAVE_COMPARE_DIGEST = False
if hasattr(hmac, 'compare_digest'):
HAVE_COMPARE_DIGEST = True
logger = logging.getLogger("web2py")
@@ -77,6 +81,8 @@ def AES_new(key, IV=None):
def compare(a, b):
""" Compares two strings and not vulnerable to timing attacks """
if HAVE_COMPARE_DIGEST:
return hmac.compare_digest(a, b)
if len(a) != len(b):
return False
result = 0
@@ -143,6 +149,7 @@ DIGEST_ALG_BY_SIZE = {
512 / 4: 'sha512',
}
def get_callable_argspec(fn):
if inspect.isfunction(fn) or inspect.ismethod(fn):
inspectable = fn
@@ -154,6 +161,7 @@ def get_callable_argspec(fn):
inspectable = fn
return inspect.getargspec(inspectable)
def pad(s, n=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):
if not ':' in data:
if ':' not in data:
return None
if not hash_key:
hash_key = sha1(encryption_key).hexdigest()