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