# -*- coding: utf-8 -*- r""" werkzeug.contrib.securecookie ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module implements a cookie that is not alterable from the client because it adds a checksum the server checks for. You can use it as session replacement if all you have is a user id or something to mark a logged in user. Keep in mind that the data is still readable from the client as a normal cookie is. However you don't have to store and flush the sessions you have at the server. Example usage: >>> from werkzeug.contrib.securecookie import SecureCookie >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef") Dumping into a string so that one can store it in a cookie: >>> value = x.serialize() Loading from that string again: >>> x = SecureCookie.unserialize(value, "deadbeef") >>> x["baz"] (1, 2, 3) If someone modifies the cookie and the checksum is wrong the unserialize method will fail silently and return a new empty `SecureCookie` object. Keep in mind that the values will be visible in the cookie so do not store data in a cookie you don't want the user to see. Application Integration ======================= If you are using the werkzeug request objects you could integrate the secure cookie into your application like this:: from werkzeug.utils import cached_property from werkzeug.wrappers import BaseRequest from werkzeug.contrib.securecookie import SecureCookie # don't use this key but a different one; you could just use # os.urandom(20) to get something random SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea' class Request(BaseRequest): @cached_property def client_session(self): data = self.cookies.get('session_data') if not data: return SecureCookie(secret_key=SECRET_KEY) return SecureCookie.unserialize(data, SECRET_KEY) def application(environ, start_response): request = Request(environ) # get a response object here response = ... if request.client_session.should_save: session_data = request.client_session.serialize() response.set_cookie('session_data', session_data, httponly=True) return response(environ, start_response) A less verbose integration can be achieved by using shorthand methods:: class Request(BaseRequest): @cached_property def client_session(self): return SecureCookie.load_cookie(self, secret_key=COOKIE_SECRET) def application(environ, start_response): request = Request(environ) # get a response object here response = ... request.client_session.save_cookie(response) return response(environ, start_response) :copyright: 2007 Pallets :license: BSD-3-Clause """ import base64 import pickle import warnings from hashlib import sha1 as _default_hash from hmac import new as hmac from time import time from .._compat import iteritems from .._compat import text_type from .._compat import to_bytes from .._compat import to_native from .._internal import _date_to_unix from ..contrib.sessions import ModificationTrackingDict from ..security import safe_str_cmp from ..urls import url_quote_plus from ..urls import url_unquote_plus warnings.warn( "'werkzeug.contrib.securecookie' is deprecated as of version 0.15" " and will be removed in version 1.0. It has moved to" " https://github.com/pallets/secure-cookie.", DeprecationWarning, stacklevel=2, ) class UnquoteError(Exception): """Internal exception used to signal failures on quoting.""" class SecureCookie(ModificationTrackingDict): """Represents a secure cookie. You can subclass this class and provide an alternative mac method. The import thing is that the mac method is a function with a similar interface to the hashlib. Required methods are update() and digest(). Example usage: >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef") >>> x["foo"] 42 >>> x["baz"] (1, 2, 3) >>> x["blafasel"] = 23 >>> x.should_save True :param data: the initial data. Either a dict, list of tuples or `None`. :param secret_key: the secret key. If not set `None` or not specified it has to be set before :meth:`serialize` is called. :param new: The initial value of the `new` flag. """ #: The hash method to use. This has to be a module with a new function #: or a function that creates a hashlib object. Such as `hashlib.md5` #: Subclasses can override this attribute. The default hash is sha1. #: Make sure to wrap this in staticmethod() if you store an arbitrary #: function there such as hashlib.sha1 which might be implemented #: as a function. hash_method = staticmethod(_default_hash) #: The module used for serialization. Should have a ``dumps`` and a #: ``loads`` method that takes bytes. The default is :mod:`pickle`. #: #: .. versionchanged:: 0.15 #: The default of ``pickle`` will change to :mod:`json` in 1.0. serialization_method = pickle #: if the contents should be base64 quoted. This can be disabled if the #: serialization process returns cookie safe strings only. quote_base64 = True def __init__(self, data=None, secret_key=None, new=True): ModificationTrackingDict.__init__(self, data or ()) # explicitly convert it into a bytestring because python 2.6 # no longer performs an implicit string conversion on hmac if secret_key is not None: secret_key = to_bytes(secret_key, "utf-8") self.secret_key = secret_key self.new = new if self.serialization_method is pickle: warnings.warn( "The default 'SecureCookie.serialization_method' will" " change from pickle to json in version 1.0. To upgrade" " existing tokens, override 'unquote' to try pickle if" " json fails.", stacklevel=2, ) def __repr__(self): return "<%s %s%s>" % ( self.__class__.__name__, dict.__repr__(self), "*" if self.should_save else "", ) @property def should_save(self): """True if the session should be saved. By default this is only true for :attr:`modified` cookies, not :attr:`new`. """ return self.modified @classmethod def quote(cls, value): """Quote the value for the cookie. This can be any object supported by :attr:`serialization_method`. :param value: the value to quote. """ if cls.serialization_method is not None: value = cls.serialization_method.dumps(value) if cls.quote_base64: value = b"".join( base64.b64encode(to_bytes(value, "utf8")).splitlines() ).strip() return value @classmethod def unquote(cls, value): """Unquote the value for the cookie. If unquoting does not work a :exc:`UnquoteError` is raised. :param value: the value to unquote. """ try: if cls.quote_base64: value = base64.b64decode(value) if cls.serialization_method is not None: value = cls.serialization_method.loads(value) return value except Exception: # unfortunately pickle and other serialization modules can # cause pretty every error here. if we get one we catch it # and convert it into an UnquoteError raise UnquoteError() def serialize(self, expires=None): """Serialize the secure cookie into a string. If expires is provided, the session will be automatically invalidated after expiration when you unseralize it. This provides better protection against session cookie theft. :param expires: an optional expiration date for the cookie (a :class:`datetime.datetime` object) """ if self.secret_key is None: raise RuntimeError("no secret key defined") if expires: self["_expires"] = _date_to_unix(expires) result = [] mac = hmac(self.secret_key, None, self.hash_method) for key, value in sorted(self.items()): result.append( ( "%s=%s" % (url_quote_plus(key), self.quote(value).decode("ascii")) ).encode("ascii") ) mac.update(b"|" + result[-1]) return b"?".join([base64.b64encode(mac.digest()).strip(), b"&".join(result)]) @classmethod def unserialize(cls, string, secret_key): """Load the secure cookie from a serialized string. :param string: the cookie value to unserialize. :param secret_key: the secret key used to serialize the cookie. :return: a new :class:`SecureCookie`. """ if isinstance(string, text_type): string = string.encode("utf-8", "replace") if isinstance(secret_key, text_type): secret_key = secret_key.encode("utf-8", "replace") try: base64_hash, data = string.split(b"?", 1) except (ValueError, IndexError): items = () else: items = {} mac = hmac(secret_key, None, cls.hash_method) for item in data.split(b"&"): mac.update(b"|" + item) if b"=" not in item: items = None break key, value = item.split(b"=", 1) # try to make the key a string key = url_unquote_plus(key.decode("ascii")) try: key = to_native(key) except UnicodeError: pass items[key] = value # no parsing error and the mac looks okay, we can now # sercurely unpickle our cookie. try: client_hash = base64.b64decode(base64_hash) except TypeError: items = client_hash = None if items is not None and safe_str_cmp(client_hash, mac.digest()): try: for key, value in iteritems(items): items[key] = cls.unquote(value) except UnquoteError: items = () else: if "_expires" in items: if time() > items["_expires"]: items = () else: del items["_expires"] else: items = () return cls(items, secret_key, False) @classmethod def load_cookie(cls, request, key="session", secret_key=None): """Loads a :class:`SecureCookie` from a cookie in request. If the cookie is not set, a new :class:`SecureCookie` instanced is returned. :param request: a request object that has a `cookies` attribute which is a dict of all cookie values. :param key: the name of the cookie. :param secret_key: the secret key used to unquote the cookie. Always provide the value even though it has no default! """ data = request.cookies.get(key) if not data: return cls(secret_key=secret_key) return cls.unserialize(data, secret_key) def save_cookie( self, response, key="session", expires=None, session_expires=None, max_age=None, path="/", domain=None, secure=None, httponly=False, force=False, ): """Saves the SecureCookie in a cookie on response object. All parameters that are not described here are forwarded directly to :meth:`~BaseResponse.set_cookie`. :param response: a response object that has a :meth:`~BaseResponse.set_cookie` method. :param key: the name of the cookie. :param session_expires: the expiration date of the secure cookie stored information. If this is not provided the cookie `expires` date is used instead. """ if force or self.should_save: data = self.serialize(session_expires or expires) response.set_cookie( key, data, expires=expires, max_age=max_age, path=path, domain=domain, secure=secure, httponly=httponly, )