1ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik# -*- coding: utf-8 -*-
2ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik"""
3ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    webapp2_extras.securecookie
4ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    ===========================
5ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
6ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    A serializer for signed cookies.
7ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
8ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    :copyright: 2011 by tipfy.org.
9ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    :license: Apache Sotware License, see LICENSE for details.
10ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik"""
11ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikimport Cookie
12ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikimport hashlib
13ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikimport hmac
14ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikimport logging
15ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikimport time
16ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
17ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikfrom webapp2_extras import json
18ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikfrom webapp2_extras import security
19ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
20ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
21ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craikclass SecureCookieSerializer(object):
22ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    """Serializes and deserializes secure cookie values.
23ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
24ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    Extracted from `Tornado`_ and modified.
25ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    """
26ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
27ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    def __init__(self, secret_key):
28ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        """Initiliazes the serializer/deserializer.
29ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
30ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :param secret_key:
31ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            A random string to be used as the HMAC secret for the cookie
32ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            signature.
33ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        """
34ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        self.secret_key = secret_key
35ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
36ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    def serialize(self, name, value):
37ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        """Serializes a signed cookie value.
38ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
39ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :param name:
40ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            Cookie name.
41ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :param value:
42ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            Cookie value to be serialized.
43ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :returns:
44ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            A serialized value ready to be stored in a cookie.
45ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        """
46ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        timestamp = str(self._get_timestamp())
47ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        value = self._encode(value)
48ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        signature = self._get_signature(name, value, timestamp)
49ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        return '|'.join([value, timestamp, signature])
50ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
51ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    def deserialize(self, name, value, max_age=None):
52ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        """Deserializes a signed cookie value.
53ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
54ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :param name:
55ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            Cookie name.
56ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :param value:
57ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            A cookie value to be deserialized.
58ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :param max_age:
59ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            Maximum age in seconds for a valid cookie. If the cookie is older
60ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            than this, returns None.
61ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        :returns:
62ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            The deserialized secure cookie, or None if it is not valid.
63ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        """
64ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        if not value:
65ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            return None
66ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
67ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        # Unquote for old WebOb.
68ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        value = Cookie._unquote(value)
69ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
70ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        parts = value.split('|')
71ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        if len(parts) != 3:
72ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            return None
73ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
74ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        signature = self._get_signature(name, parts[0], parts[1])
75ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
76ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        if not security.compare_hashes(parts[2], signature):
77ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            logging.warning('Invalid cookie signature %r', value)
78ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            return None
79ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
80ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        if max_age is not None:
81ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            if int(parts[1]) < self._get_timestamp() - max_age:
82ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik                logging.warning('Expired cookie %r', value)
83ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik                return None
84ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
85ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        try:
86ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            return self._decode(parts[0])
87ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        except Exception, e:
88ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            logging.warning('Cookie value failed to be decoded: %r', parts[0])
89ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik            return None
90ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
91ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    def _encode(self, value):
92ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        return json.b64encode(value)
93ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
94ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    def _decode(self, value):
95ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        return json.b64decode(value)
96ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
97ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    def _get_timestamp(self):
98ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        return int(time.time())
99ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik
100ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik    def _get_signature(self, *parts):
101ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        """Generates an HMAC signature."""
102ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        signature = hmac.new(self.secret_key, digestmod=hashlib.sha1)
103ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        signature.update('|'.join(parts))
104ced05db70069f9d84c4b0dd9b3b26b94e3482336Chris Craik        return signature.hexdigest()
105