diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 214a9a2d..c7e1f73c 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -6,7 +6,7 @@ try: except ImportError: import pickle -import hashlib +import hmac import secrets import time @@ -218,13 +218,16 @@ class MailuSession(CallbackDict, SessionMixin): def save(self): """ Save session to store. """ + set_cookie = False + # set uid from dict data if self._uid is None: self._uid = self.app.session_config.gen_uid(self.get('user_id', '')) - # create new session id for new or regenerated sessions + # create new session id for new or regenerated sessions and force setting the cookie if self._sid is None: self._sid = self.app.session_config.gen_sid() + set_cookie = True # get new session key key = self.sid @@ -233,6 +236,9 @@ class MailuSession(CallbackDict, SessionMixin): if key != self._key: self.delete() + # remember time to refresh + self['_refresh'] = int(time.time()) + self.app.permanent_session_lifetime.total_seconds()/2 + # save session self.app.session_store.put( key, @@ -245,37 +251,52 @@ class MailuSession(CallbackDict, SessionMixin): self.new = False self.modified = False + return set_cookie + + def needs_refresh(self): + """ Checks if server side session needs to be refreshed. """ + + return int(time.time()) > self.get('_refresh', 0) + class MailuSessionConfig: """ Stores sessions crypto config """ + # default size of session key parts + uid_bits = 64 # default if SESSION_KEY_BITS is not set in config + sid_bits = 128 # for now. must be multiple of 8! + time_bits = 32 # for now. must be multiple of 8! + def __init__(self, app=None): if app is None: app = flask.current_app - bits = app.config.get('SESSION_KEY_BITS', 64) + bits = app.config.get('SESSION_KEY_BITS', self.uid_bits) + if not 64 <= bits <= 256: + raise ValueError('SESSION_KEY_BITS must be between 64 and 256!') - if bits < 64: - raise ValueError('Session id entropy must not be less than 64 bits!') + uid_bytes = bits//8 + (bits%8>0) + sid_bytes = self.sid_bits//8 - hash_bytes = bits//8 + (bits%8>0) - time_bytes = 4 # 32 bit timestamp for now + key = want_bytes(app.secret_key) - self._shaker = hashlib.shake_128(want_bytes(app.config.get('SECRET_KEY', ''))) - self._hash_len = hash_bytes - self._hash_b64 = len(self._encode(bytes(hash_bytes))) - self._key_min = 2*self._hash_b64 - self._key_max = self._key_min + len(self._encode(bytes(time_bytes))) + self._hmac = hmac.new(hmac.digest(key, key, digest='sha256'), digestmod='sha256') + self._uid_len = uid_bytes + self._uid_b64 = len(self._encode(bytes(uid_bytes))) + self._sid_len = sid_bytes + self._sid_b64 = len(self._encode(bytes(sid_bytes))) + self._key_min = self._uid_b64 + self._sid_b64 + self._key_max = self._key_min + len(self._encode(bytes(self.time_bits//8))) def gen_sid(self): """ Generate random session id. """ - return self._encode(secrets.token_bytes(self._hash_len)) + return self._encode(secrets.token_bytes(self._sid_len)) def gen_uid(self, uid): """ Generate hashed user id part of session key. """ - shaker = self._shaker.copy() - shaker.update(want_bytes(uid)) - return self._encode(shaker.digest(self._hash_len)) + _hmac = self._hmac.copy() + _hmac.update(want_bytes(uid)) + return self._encode(_hmac.digest()[:self._uid_len]) def gen_created(self, now=None): """ Generate base64 representation of creation time. """ @@ -287,8 +308,8 @@ class MailuSessionConfig: if not (isinstance(key, bytes) and self._key_min <= len(key) <= self._key_max): return None - uid = key[:self._hash_b64] - sid = key[self._hash_b64:self._key_min] + uid = key[:self._uid_b64] + sid = key[self._uid_b64:self._key_min] crt = key[self._key_min:] # validate if parts are decodeable @@ -301,7 +322,7 @@ class MailuSessionConfig: if now is None: now = int(time.time()) created = int.from_bytes(created, byteorder='big') - if not (created < now < created + app.permanent_session_lifetime.total_seconds()): + if not created < now < created + app.permanent_session_lifetime.total_seconds(): return None return (uid, sid, crt) @@ -341,24 +362,29 @@ class MailuSessionInterface(SessionInterface): if session.accessed: response.vary.add('Cookie') - # TODO: set cookie from time to time to prevent expiration in browser - # also update expire in redis + set_cookie = session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST'] + need_refresh = session.needs_refresh() - if not self.should_set_cookie(app, session): - return + # save modified session or refresh unmodified session + if session.modified or need_refresh: + set_cookie |= session.save() - # save session and update cookie - session.save() - response.set_cookie( - app.session_cookie_name, - session.sid, - expires=self.get_expiration_time(app, session), - httponly=self.get_cookie_httponly(app), - domain=self.get_cookie_domain(app), - path=self.get_cookie_path(app), - secure=self.get_cookie_secure(app), - samesite=self.get_cookie_samesite(app) - ) + # set cookie on refreshed permanent sessions + if need_refresh and session.permanent: + set_cookie = True + + # set or update cookie if necessary + if set_cookie: + response.set_cookie( + app.session_cookie_name, + session.sid, + expires=self.get_expiration_time(app, session), + httponly=self.get_cookie_httponly(app), + domain=self.get_cookie_domain(app), + path=self.get_cookie_path(app), + secure=self.get_cookie_secure(app), + samesite=self.get_cookie_samesite(app) + ) class MailuSessionExtension: """ Server side session handling """