Back to index

moin  1.9.0~rc2
securecookie.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 r"""
00003     werkzeug.contrib.securecookie
00004     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
00005 
00006     This module implements a cookie that is not alterable from the client
00007     because it adds a checksum the server checks for.  You can use it as
00008     session replacement if all you have is a user id or something to mark
00009     a logged in user.
00010 
00011     Keep in mind that the data is still readable from the client as a
00012     normal cookie is.  However you don't have to store and flush the
00013     sessions you have at the server.
00014 
00015     Example usage:
00016 
00017     >>> from werkzeug.contrib.securecookie import SecureCookie
00018     >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
00019 
00020     Dumping into a string so that one can store it in a cookie:
00021 
00022     >>> value = x.serialize()
00023 
00024     Loading from that string again:
00025 
00026     >>> x = SecureCookie.unserialize(value, "deadbeef")
00027     >>> x["baz"]
00028     (1, 2, 3)
00029 
00030     If someone modifies the cookie and the checksum is wrong the unserialize
00031     method will fail silently and return a new empty `SecureCookie` object.
00032 
00033     Keep in mind that the values will be visible in the cookie so do not
00034     store data in a cookie you don't want the user to see.
00035 
00036     Application Integration
00037     =======================
00038 
00039     If you are using the werkzeug request objects you could integrate the
00040     secure cookie into your application like this::
00041 
00042         from werkzeug import BaseRequest, cached_property
00043         from werkzeug.contrib.securecookie import SecureCookie
00044 
00045         # don't use this key but a different one; you could just use
00046         # os.unrandom(20) to get something random
00047         SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea'
00048 
00049         class Request(BaseRequest):
00050 
00051             @cached_property
00052             def client_session(self):
00053                 data = self.cookies.get('session_data')
00054                 if not data:
00055                     return SecureCookie(secret_key=SECRET_KEY)
00056                 return SecureCookie.unserialize(data, SECRET_KEY)
00057 
00058         def application(environ, start_response):
00059             request = Request(environ, start_response)
00060 
00061             # get a response object here
00062             response = ...
00063 
00064             if request.client_session.should_save:
00065                 session_data = request.client_session.serialize()
00066                 response.set_cookie('session_data', session_data,
00067                                     httponly=True)
00068             return response(environ, start_response)
00069 
00070     A less verbose integration can be achieved by using shorthand methods::
00071 
00072         class Request(BaseRequest):
00073 
00074             @cached_property
00075             def client_session(self):
00076                 return SecureCookie.load_cookie(self, secret_key=COOKIE_SECRET)
00077 
00078         def application(environ, start_response):
00079             request = Request(environ, start_response)
00080 
00081             # get a response object here
00082             response = ...
00083 
00084             request.client_session.save_cookie(response)
00085             return response(environ, start_response)
00086 
00087     :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
00088     :license: BSD, see LICENSE for more details.
00089 """
00090 import sys
00091 import cPickle as pickle
00092 from hmac import new as hmac
00093 from datetime import datetime
00094 from time import time, mktime, gmtime
00095 from werkzeug import url_quote_plus, url_unquote_plus
00096 from werkzeug._internal import _date_to_unix
00097 from werkzeug.contrib.sessions import ModificationTrackingDict
00098 
00099 
00100 # rather ugly way to import the correct hash method.  Because
00101 # hmac either accepts modules with a new method (sha, md5 etc.)
00102 # or a hashlib factory function we have to figure out what to
00103 # pass to it.  If we have 2.5 or higher (so not 2.4 with a
00104 # custom hashlib) we import from hashlib and fail if it does
00105 # not exist (have seen that in old OS X versions).
00106 # in all other cases the now deprecated sha module is used.
00107 _default_hash = None
00108 if sys.version_info >= (2, 5):
00109     try:
00110         from hashlib import sha1 as _default_hash
00111     except ImportError:
00112         pass
00113 if _default_hash is None:
00114     import sha as _default_hash
00115 
00116 
00117 class UnquoteError(Exception):
00118     """Internal exception used to signal failures on quoting."""
00119 
00120 
00121 class SecureCookie(ModificationTrackingDict):
00122     """Represents a secure cookie.  You can subclass this class and provide
00123     an alternative mac method.  The import thing is that the mac method
00124     is a function with a similar interface to the hashlib.  Required
00125     methods are update() and digest().
00126 
00127     Example usage:
00128 
00129     >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
00130     >>> x["foo"]
00131     42
00132     >>> x["baz"]
00133     (1, 2, 3)
00134     >>> x["blafasel"] = 23
00135     >>> x.should_save
00136     True
00137 
00138     :param data: the initial data.  Either a dict, list of tuples or `None`.
00139     :param secret_key: the secret key.  If not set `None` or not specified
00140                        it has to be set before :meth:`serialize` is called.
00141     :param new: The initial value of the `new` flag.
00142     """
00143 
00144     #: The hash method to use.  This has to be a module with a new function
00145     #: or a function that creates a hashlib object.  Such as `hashlib.md5`
00146     #: Subclasses can override this attribute.  The default hash is sha1.
00147     hash_method = _default_hash
00148 
00149     #: the module used for serialization.  Unless overriden by subclasses
00150     #: the standard pickle module is used.
00151     serialization_method = pickle
00152 
00153     #: if the contents should be base64 quoted.  This can be disabled if the
00154     #: serialization process returns cookie safe strings only.
00155     quote_base64 = True
00156 
00157     def __init__(self, data=None, secret_key=None, new=True):
00158         ModificationTrackingDict.__init__(self, data or ())
00159         # explicitly convert it into a bytestring because python 2.6
00160         # no longer performs an implicit string conversion on hmac
00161         if secret_key is not None:
00162             secret_key = str(secret_key)
00163         self.secret_key = secret_key
00164         self.new = new
00165 
00166     def __repr__(self):
00167         return '<%s %s%s>' % (
00168             self.__class__.__name__,
00169             dict.__repr__(self),
00170             self.should_save and '*' or ''
00171         )
00172 
00173     def should_save(self):
00174         """True if the session should be saved.  By default this is only true
00175         for :attr:`modified` cookies, not :attr:`new`.
00176         """
00177         return self.modified
00178     should_save = property(should_save, doc=should_save.__doc__)
00179 
00180     @classmethod
00181     def quote(cls, value):
00182         """Quote the value for the cookie.  This can be any object supported
00183         by :attr:`serialization_method`.
00184 
00185         :param value: the value to quote.
00186         """
00187         if cls.serialization_method is not None:
00188             value = cls.serialization_method.dumps(value)
00189         if cls.quote_base64:
00190             value = ''.join(value.encode('base64').splitlines()).strip()
00191         return value
00192 
00193     @classmethod
00194     def unquote(cls, value):
00195         """Unquote the value for the cookie.  If unquoting does not work a
00196         :exc:`UnquoteError` is raised.
00197 
00198         :param value: the value to unquote.
00199         """
00200         try:
00201             if cls.quote_base64:
00202                 value = value.decode('base64')
00203             if cls.serialization_method is not None:
00204                 value = cls.serialization_method.loads(value)
00205             return value
00206         except:
00207             # unfortunately pickle and other serialization modules can
00208             # cause pretty every error here.  if we get one we catch it
00209             # and convert it into an UnquoteError
00210             raise UnquoteError()
00211 
00212     def serialize(self, expires=None):
00213         """Serialize the secure cookie into a string.
00214 
00215         If expires is provided, the session will be automatically invalidated
00216         after expiration when you unseralize it. This provides better
00217         protection against session cookie theft.
00218 
00219         :param expires: an optional expiration date for the cookie (a
00220                         :class:`datetime.datetime` object)
00221         """
00222         if self.secret_key is None:
00223             raise RuntimeError('no secret key defined')
00224         if expires:
00225             self['_expires'] = _date_to_unix(expires)
00226         result = []
00227         mac = hmac(self.secret_key, None, self.hash_method)
00228         for key, value in sorted(self.items()):
00229             result.append('%s=%s' % (
00230                 url_quote_plus(key),
00231                 self.quote(value)
00232             ))
00233             mac.update('|' + result[-1])
00234         return '%s?%s' % (
00235             mac.digest().encode('base64').strip(),
00236             '&'.join(result)
00237         )
00238 
00239     @classmethod
00240     def unserialize(cls, string, secret_key):
00241         """Load the secure cookie from a serialized string.
00242 
00243         :param string: the cookie value to unserialize.
00244         :param secret_key: the secret key used to serialize the cookie.
00245         :return: a new :class:`SecureCookie`.
00246         """
00247         if isinstance(string, unicode):
00248             string = string.encode('utf-8', 'ignore')
00249         try:
00250             base64_hash, data = string.split('?', 1)
00251         except (ValueError, IndexError):
00252             items = ()
00253         else:
00254             items = {}
00255             mac = hmac(secret_key, None, cls.hash_method)
00256             for item in data.split('&'):
00257                 mac.update('|' + item)
00258                 if not '=' in item:
00259                     items = None
00260                     break
00261                 key, value = item.split('=', 1)
00262                 # try to make the key a string
00263                 key = url_unquote_plus(key)
00264                 try:
00265                     key = str(key)
00266                 except UnicodeError:
00267                     pass
00268                 items[key] = value
00269 
00270             # no parsing error and the mac looks okay, we can now
00271             # sercurely unpickle our cookie.
00272             try:
00273                 client_hash = base64_hash.decode('base64')
00274             except Exception:
00275                 items = client_hash = None
00276             if items is not None and client_hash == mac.digest():
00277                 try:
00278                     for key, value in items.iteritems():
00279                         items[key] = cls.unquote(value)
00280                 except UnquoteError:
00281                     items = ()
00282                 else:
00283                     if '_expires' in items:
00284                         if time() > items['_expires']:
00285                             items = ()
00286                         else:
00287                             del items['_expires']
00288             else:
00289                 items = ()
00290         return cls(items, secret_key, False)
00291 
00292     @classmethod
00293     def load_cookie(cls, request, key='session', secret_key=None):
00294         """Loads a :class:`SecureCookie` from a cookie in request.  If the
00295         cookie is not set, a new :class:`SecureCookie` instanced is
00296         returned.
00297 
00298         :param request: a request object that has a `cookies` attribute
00299                         which is a dict of all cookie values.
00300         :param key: the name of the cookie.
00301         :param secret_key: the secret key used to unquote the cookie.
00302                            Always provide the value even though it has
00303                            no default!
00304         """
00305         data = request.cookies.get(key)
00306         if not data:
00307             return SecureCookie(secret_key=secret_key)
00308         return SecureCookie.unserialize(data, secret_key)
00309 
00310     def save_cookie(self, response, key='session', expires=None,
00311                     session_expires=None, max_age=None, path='/', domain=None,
00312                     secure=None, httponly=False, force=False):
00313         """Saves the SecureCookie in a cookie on response object.  All
00314         parameters that are not described here are forwarded directly
00315         to :meth:`~BaseResponse.set_cookie`.
00316 
00317         :param response: a response object that has a
00318                          :meth:`~BaseResponse.set_cookie` method.
00319         :param key: the name of the cookie.
00320         :param session_expires: the expiration date of the secure cookie
00321                                 stored information.  If this is not provided
00322                                 the cookie `expires` date is used instead.
00323         """
00324         if force or self.should_save:
00325             data = self.serialize(session_expires or expires)
00326             response.set_cookie(key, data, expires=expires, max_age=max_age,
00327                                 path=path, domain=domain, secure=secure,
00328                                 httponly=httponly)