@ -6,7 +6,7 @@ try:
except ImportError :
except ImportError :
import pickle
import pickle
import h ashlib
import h mac
import secrets
import secrets
import time
import time
@ -218,13 +218,16 @@ class MailuSession(CallbackDict, SessionMixin):
def save ( self ) :
def save ( self ) :
""" Save session to store. """
""" Save session to store. """
set_cookie = False
# set uid from dict data
# set uid from dict data
if self . _uid is None :
if self . _uid is None :
self . _uid = self . app . session_config . gen_uid ( self . get ( ' user_id ' , ' ' ) )
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 :
if self . _sid is None :
self . _sid = self . app . session_config . gen_sid ( )
self . _sid = self . app . session_config . gen_sid ( )
set_cookie = True
# get new session key
# get new session key
key = self . sid
key = self . sid
@ -233,6 +236,9 @@ class MailuSession(CallbackDict, SessionMixin):
if key != self . _key :
if key != self . _key :
self . delete ( )
self . delete ( )
# remember time to refresh
self [ ' _refresh ' ] = int ( time . time ( ) ) + self . app . permanent_session_lifetime . total_seconds ( ) / 2
# save session
# save session
self . app . session_store . put (
self . app . session_store . put (
key ,
key ,
@ -245,37 +251,52 @@ class MailuSession(CallbackDict, SessionMixin):
self . new = False
self . new = False
self . modified = 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 :
class MailuSessionConfig :
""" Stores sessions crypto config """
""" 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 ) :
def __init__ ( self , app = None ) :
if app is None :
if app is None :
app = flask . current_app
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 :
uid_bytes = bits / / 8 + ( bits % 8 > 0 )
raise ValueError ( ' Session id entropy must not be less than 64 bits! ' )
sid_bytes = self . sid_bits / / 8
hash_bytes = bits / / 8 + ( bits % 8 > 0 )
key = want_bytes ( app . secret_key )
time_bytes = 4 # 32 bit timestamp for now
self . _shaker = hashlib . shake_128 ( want_bytes ( app . config . get ( ' SECRET_KEY ' , ' ' ) ) )
self . _hmac = hmac . new ( hmac . digest ( key , key , digest = ' sha256 ' ) , digestmod = ' sha256 ' )
self . _hash_len = hash_bytes
self . _uid_len = uid_bytes
self . _hash_b64 = len ( self . _encode ( bytes ( hash_bytes ) ) )
self . _uid_b64 = len ( self . _encode ( bytes ( uid_bytes ) ) )
self . _key_min = 2 * self . _hash_b64
self . _sid_len = sid_bytes
self . _key_max = self . _key_min + len ( self . _encode ( bytes ( time_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 ) :
def gen_sid ( self ) :
""" Generate random session id. """
""" 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 ) :
def gen_uid ( self , uid ) :
""" Generate hashed user id part of session key. """
""" Generate hashed user id part of session key. """
shaker = self . _shaker . copy ( )
_hmac = self . _hmac . copy ( )
shaker . update ( want_bytes ( uid ) )
_hmac . update ( want_bytes ( uid ) )
return self . _encode ( shaker. digest ( self . _hash_len ) )
return self . _encode ( _hmac. digest ( ) [ : self . _uid_len ] )
def gen_created ( self , now = None ) :
def gen_created ( self , now = None ) :
""" Generate base64 representation of creation time. """
""" 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 ) :
if not ( isinstance ( key , bytes ) and self . _key_min < = len ( key ) < = self . _key_max ) :
return None
return None
uid = key [ : self . _ hash _b64]
uid = key [ : self . _ uid _b64]
sid = key [ self . _ hash _b64: self . _key_min ]
sid = key [ self . _ uid _b64: self . _key_min ]
crt = key [ self . _key_min : ]
crt = key [ self . _key_min : ]
# validate if parts are decodeable
# validate if parts are decodeable
@ -301,7 +322,7 @@ class MailuSessionConfig:
if now is None :
if now is None :
now = int ( time . time ( ) )
now = int ( time . time ( ) )
created = int . from_bytes ( created , byteorder = ' big ' )
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 None
return ( uid , sid , crt )
return ( uid , sid , crt )
@ -341,14 +362,19 @@ class MailuSessionInterface(SessionInterface):
if session . accessed :
if session . accessed :
response . vary . add ( ' Cookie ' )
response . vary . add ( ' Cookie ' )
# TODO: set cookie from time to time to prevent expiration in browser
set_cookie = session . permanent and app . config [ ' SESSION_REFRESH_EACH_REQUEST ' ]
# also update expire in redis
need_refresh = session . needs_refresh ( )
if not self . should_set_cookie ( app , session ) :
# save modified session or refresh unmodified session
return
if session . modified or need_refresh :
set_cookie | = session . save ( )
# save session and update cookie
# set cookie on refreshed permanent sessions
session . save ( )
if need_refresh and session . permanent :
set_cookie = True
# set or update cookie if necessary
if set_cookie :
response . set_cookie (
response . set_cookie (
app . session_cookie_name ,
app . session_cookie_name ,
session . sid ,
session . sid ,