Merge pull request #1149 from niphlod/enhancement/constant_time_compare
bultin constant time checking
This commit is contained in:
+51
-59
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user