Ticket #12417: ticket12417-v7.diff

File ticket12417-v7.diff, 35.2 KB (added by Jannis Leidel, 13 years ago)

Fixed baseconv.

  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 88aa5a3..c98cab7 100644
    a b LOGIN_REDIRECT_URL = '/accounts/profile/'  
    476476# The number of days a password reset link is valid for
    477477PASSWORD_RESET_TIMEOUT_DAYS = 3
    478478
     479###########
     480# SIGNING #
     481###########
     482
     483SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
     484
    479485########
    480486# CSRF #
    481487########
  • new file django/core/signing.py

    diff --git a/django/core/signing.py b/django/core/signing.py
    new file mode 100644
    index 0000000..3b0a48b
    - +  
     1"""
     2Functions for creating and restoring url-safe signed JSON objects.
     3
     4The format used looks like this:
     5
     6>>> signed.dumps("hello")
     7'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
     8
     9There are two components here, separatad by a '.'. The first component is a
     10URLsafe base64 encoded JSON of the object passed to dumps(). The second
     11component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
     12
     13signed.loads(s) checks the signature and returns the deserialised object.
     14If the signature fails, a BadSignature exception is raised.
     15
     16>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
     17u'hello'
     18>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
     19...
     20BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
     21
     22You can optionally compress the JSON prior to base64 encoding it to save
     23space, using the compress=True argument. This checks if compression actually
     24helps and only applies compression if the result is a shorter string:
     25
     26>>> signed.dumps(range(1, 20), compress=True)
     27'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
     28
     29The fact that the string is compressed is signalled by the prefixed '.' at the
     30start of the base64 JSON.
     31
     32There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
     33These functions make use of all of them.
     34"""
     35import base64
     36import time
     37import zlib
     38
     39from django.conf import settings
     40from django.core.exceptions import ImproperlyConfigured
     41from django.utils import baseconv, simplejson
     42from django.utils.crypto import constant_time_compare, salted_hmac
     43from django.utils.encoding import force_unicode, smart_str
     44from django.utils.importlib import import_module
     45
     46
     47class BadSignature(Exception):
     48    """
     49    Signature does not match
     50    """
     51    pass
     52
     53
     54class SignatureExpired(BadSignature):
     55    """
     56    Signature timestamp is older than required max_age
     57    """
     58    pass
     59
     60
     61def b64_encode(s):
     62    return base64.urlsafe_b64encode(s).strip('=')
     63
     64
     65def b64_decode(s):
     66    pad = '=' * (-len(s) % 4)
     67    return base64.urlsafe_b64decode(s + pad)
     68
     69
     70def base64_hmac(salt, value, key):
     71    return b64_encode(salted_hmac(salt, value, key).digest())
     72
     73
     74def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
     75    modpath = settings.SIGNING_BACKEND
     76    module, attr = modpath.rsplit('.', 1)
     77    try:
     78        mod = import_module(module)
     79    except ImportError, e:
     80        raise ImproperlyConfigured(
     81            'Error importing cookie signer %s: "%s"' % (modpath, e))
     82    try:
     83        Signer = getattr(mod, attr)
     84    except AttributeError, e:
     85        raise ImproperlyConfigured(
     86            'Error importing cookie signer %s: "%s"' % (modpath, e))
     87    return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt)
     88
     89
     90def dumps(obj, key=None, salt='django.core.signing', compress=False):
     91    """
     92    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
     93    None, settings.SECRET_KEY is used instead.
     94
     95    If compress is True (not the default) checks if compressing using zlib can
     96    save some space. Prepends a '.' to signify compression. This is included
     97    in the signature, to protect against zip bombs.
     98
     99    salt can be used to further salt the hash, in case you're worried
     100    that the NSA might try to brute-force your SHA-1 protected secret.
     101    """
     102    json = simplejson.dumps(obj, separators=(',', ':'))
     103
     104    # Flag for if it's been compressed or not
     105    is_compressed = False
     106
     107    if compress:
     108        # Avoid zlib dependency unless compress is being used
     109        compressed = zlib.compress(json)
     110        if len(compressed) < (len(json) - 1):
     111            json = compressed
     112            is_compressed = True
     113    base64d = b64_encode(json)
     114    if is_compressed:
     115        base64d = '.' + base64d
     116    return TimestampSigner(key, salt=salt).sign(base64d)
     117
     118
     119def loads(s, key=None, salt='django.core.signing', max_age=None):
     120    """
     121    Reverse of dumps(), raises BadSignature if signature fails
     122    """
     123    base64d = smart_str(
     124        TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
     125    decompress = False
     126    if base64d[0] == '.':
     127        # It's compressed; uncompress it first
     128        base64d = base64d[1:]
     129        decompress = True
     130    json = b64_decode(base64d)
     131    if decompress:
     132        json = zlib.decompress(json)
     133    return simplejson.loads(json)
     134
     135
     136class Signer(object):
     137    def __init__(self, key=None, sep=':', salt=None):
     138        self.sep = sep
     139        self.key = key or settings.SECRET_KEY
     140        self.salt = salt or ('%s.%s' %
     141            (self.__class__.__module__, self.__class__.__name__))
     142
     143    def signature(self, value):
     144        return base64_hmac(self.salt + 'signer', value, self.key)
     145
     146    def sign(self, value):
     147        value = smart_str(value)
     148        return '%s%s%s' % (value, self.sep, self.signature(value))
     149
     150    def unsign(self, signed_value):
     151        signed_value = smart_str(signed_value)
     152        if not self.sep in signed_value:
     153            raise BadSignature('No "%s" found in value' % self.sep)
     154        value, sig = signed_value.rsplit(self.sep, 1)
     155        if constant_time_compare(sig, self.signature(value)):
     156            return force_unicode(value)
     157        raise BadSignature('Signature "%s" does not match' % sig)
     158
     159
     160class TimestampSigner(Signer):
     161    def timestamp(self):
     162        return baseconv.base62.encode(int(time.time()))
     163
     164    def sign(self, value):
     165        value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
     166        return '%s%s%s' % (value, self.sep, self.signature(value))
     167
     168    def unsign(self, value, max_age=None):
     169        result =  super(TimestampSigner, self).unsign(value)
     170        value, timestamp = result.rsplit(self.sep, 1)
     171        timestamp = baseconv.base62.decode(timestamp)
     172        if max_age is not None:
     173            # Check timestamp is not older than max_age
     174            age = time.time() - timestamp
     175            if age > max_age:
     176                raise SignatureExpired(
     177                    'Signature age %s > %s seconds' % (age, max_age))
     178        return value
  • django/http/__init__.py

    diff --git a/django/http/__init__.py b/django/http/__init__.py
    index 0d28ec0..a3fd7f5 100644
    a b from django.utils.encoding import smart_str, iri_to_uri, force_unicode  
    122122from django.utils.http import cookie_date
    123123from django.http.multipartparser import MultiPartParser
    124124from django.conf import settings
     125from django.core import signing
    125126from django.core.files import uploadhandler
    126127from utils import *
    127128
    absolute_http_url_re = re.compile(r"^https?://", re.I)  
    132133class Http404(Exception):
    133134    pass
    134135
     136RAISE_ERROR = object()
     137
    135138class HttpRequest(object):
    136139    """A basic HTTP request."""
    137140
    class HttpRequest(object):  
    170173        # Rather than crash if this doesn't happen, we encode defensively.
    171174        return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '')
    172175
     176    def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None):
     177        """
     178        Attempts to return a signed cookie. If the signature fails or the
     179        cookie has expired, raises an exception... unless you provide the
     180        default argument in which case that value will be returned instead.
     181        """
     182        try:
     183            cookie_value = self.COOKIES[key].encode('utf-8')
     184        except KeyError:
     185            if default is not RAISE_ERROR:
     186                return default
     187            else:
     188                raise
     189        try:
     190            value = signing.get_cookie_signer(salt=key + salt).unsign(
     191                cookie_value, max_age=max_age)
     192        except signing.BadSignature:
     193            if default is not RAISE_ERROR:
     194                return default
     195            else:
     196                raise
     197        return value
     198
    173199    def build_absolute_uri(self, location=None):
    174200        """
    175201        Builds an absolute URI from the location and the variables available in
    class HttpResponse(object):  
    584610        if httponly:
    585611            self.cookies[key]['httponly'] = True
    586612
     613    def set_signed_cookie(self, key, value, salt='', **kwargs):
     614        value = signing.get_cookie_signer(salt=key + salt).sign(value)
     615        return self.set_cookie(key, value, **kwargs)
     616
    587617    def delete_cookie(self, key, path='/', domain=None):
    588618        self.set_cookie(key, max_age=0, path=path, domain=domain,
    589619                        expires='Thu, 01-Jan-1970 00:00:00 GMT')
    def str_to_unicode(s, encoding):  
    686716        return unicode(s, encoding, 'replace')
    687717    else:
    688718        return s
    689 
  • new file django/utils/baseconv.py

    diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py
    new file mode 100644
    index 0000000..702c172
    - +  
     1# Copyright (c) 2010 Taurinus Collective. All rights reserved.
     2# Copyright (c) 2009 Simon Willison. All rights reserved.
     3# Copyright (c) 2002 Drew Perttula. All rights reserved.
     4#
     5# License:
     6#   Python Software Foundation License version 2
     7#
     8# See the file "LICENSE" for terms & conditions for usage, and a DISCLAIMER OF
     9# ALL WARRANTIES.
     10#
     11# This Baseconv distribution contains no GNU General Public Licensed (GPLed)
     12# code so it may be used in proprietary projects just like prior ``baseconv``
     13# distributions.
     14#
     15# All trademarks referenced herein are property of their respective holders.
     16#
     17
     18"""
     19Convert numbers from base 10 integers to base X strings and back again.
     20
     21Sample usage::
     22
     23  >>> base20 = BaseConverter('0123456789abcdefghij')
     24  >>> base20.encode(1234)
     25  '31e'
     26  >>> base20.decode('31e')
     27  1234
     28  >>> base20.encode(-1234)
     29  '-31e'
     30  >>> base20.decode('-31e')
     31  -1234
     32  >>> base11 = BaseConverter('0123456789-', sign='$')
     33  >>> base11.encode('$1234')
     34  '$-22'
     35  >>> base11.decode('$-22')
     36  '$1234'
     37
     38"""
     39
     40BASE2_ALPHABET = '01'
     41BASE16_ALPHABET = '0123456789ABCDEF'
     42BASE56_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz'
     43BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
     44BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
     45BASE64_ALPHABET = BASE62_ALPHABET + '-_'
     46
     47class BaseConverter(object):
     48    decimal_digits = '0123456789'
     49
     50    def __init__(self, digits, sign='-'):
     51        self.sign = sign
     52        self.digits = digits
     53        if sign in self.digits:
     54            raise ValueError('Sign character found in converter base digits.')
     55
     56    def __repr__(self):
     57        return "<BaseConverter: base%s (%s)>" % (len(self.digits), self.digits)
     58
     59    def encode(self, i):
     60        neg, value = self.convert(i, self.decimal_digits, self.digits, '-')
     61        if neg:
     62            return self.sign + value
     63        return value
     64
     65    def decode(self, s):
     66        neg, value = self.convert(s, self.digits, self.decimal_digits, self.sign)
     67        if neg:
     68            value = '-' + value
     69        return int(value)
     70
     71    def convert(self, number, from_digits, to_digits, sign):
     72        if str(number)[0] == sign:
     73            number = str(number)[1:]
     74            neg = 1
     75        else:
     76            neg = 0
     77
     78        # make an integer out of the number
     79        x = 0
     80        for digit in str(number):
     81            x = x * len(from_digits) + from_digits.index(digit)
     82
     83        # create the result in base 'len(to_digits)'
     84        if x == 0:
     85            res = to_digits[0]
     86        else:
     87            res = ''
     88            while x > 0:
     89                digit = x % len(to_digits)
     90                res = to_digits[digit] + res
     91                x = int(x / len(to_digits))
     92        return neg, res
     93
     94base2 = BaseConverter(BASE2_ALPHABET)
     95base16 = BaseConverter(BASE16_ALPHABET)
     96base36 = BaseConverter(BASE36_ALPHABET)
     97base56 = BaseConverter(BASE56_ALPHABET)
     98base62 = BaseConverter(BASE62_ALPHABET)
     99base64 = BaseConverter(BASE64_ALPHABET, sign='$')
  • docs/index.txt

    diff --git a/docs/index.txt b/docs/index.txt
    index 9135d32..8b4ae53 100644
    a b Other batteries included  
    171171    * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
    172172    * :doc:`Content types <ref/contrib/contenttypes>`
    173173    * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
     174    * :doc:`Cryptographic signing <topics/signing>`
    174175    * :doc:`Databrowse <ref/contrib/databrowse>`
    175176    * :doc:`E-mail (sending) <topics/email>`
    176177    * :doc:`Flatpages <ref/contrib/flatpages>`
  • docs/ref/request-response.txt

    diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
    index 6281120..e17c0a7 100644
    a b Methods  
    240240
    241241   Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
    242242
     243.. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None)
     244
     245   .. versionadded:: 1.4
     246
     247   Returns a cookie value for a signed cookie, or raises a
     248   :class:`~django.core.signing.BadSignature` exception if the signature is
     249   no longer valid. If you provide the ``default`` argument the exception
     250   will be suppressed and that default value will be returned instead.
     251
     252   The optional ``salt`` argument can be used to provide extra protection
     253   against brute force attacks on your secret key. If supplied, the
     254   ``max_age`` argument will be checked against the signed timestamp
     255   attached to the cookie value to ensure the cookie is not older than
     256   ``max_age`` seconds.
     257
     258   For example::
     259
     260          >>> request.get_signed_cookie('name')
     261          'Tony'
     262          >>> request.get_signed_cookie('name', salt='name-salt')
     263          'Tony' # assuming cookie was set using the same salt
     264          >>> request.get_signed_cookie('non-existing-cookie')
     265          ...
     266          KeyError: 'non-existing-cookie'
     267          >>> request.get_signed_cookie('non-existing-cookie', False)
     268          False
     269          >>> request.get_signed_cookie('cookie-that-was-tampered-with')
     270          ...
     271          BadSignature: ...
     272          >>> request.get_signed_cookie('name', max_age=60)
     273          ...
     274          SignatureExpired: Signature age 1677.3839159 > 60 seconds
     275          >>> request.get_signed_cookie('name', False, max_age=60)
     276          False
     277
     278   See :ref:`cryptographic signing <topics-signing>` for more information.
     279
    243280.. method:: HttpRequest.is_secure()
    244281
    245282   Returns ``True`` if the request is secure; that is, if it was made with
    Methods  
    618655    .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
    619656    .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
    620657
     658.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
     659
     660    .. versionadded:: 1.4
     661
     662    Like :meth:`~HttpResponse.set_cookie()`, but
     663    :ref:`cryptographically signs <topics-signing>` the cookie before setting
     664    it. Use in conjunction with :meth:`HttpRequest.get_signed_cookie`.
     665    You can use the optional ``salt`` argument for added key strength, but
     666    you will need to remember to pass it to the corresponding
     667    :meth:`HttpRequest.get_signed_cookie` call.
     668
    621669.. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
    622670
    623671    Deletes the cookie with the given key. Fails silently if the key doesn't
  • docs/ref/settings.txt

    diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
    index f5f1226..38977e8 100644
    a b See :tfilter:`allowed date format strings <date>`.  
    16471647
    16481648See also ``DATE_FORMAT`` and ``SHORT_DATETIME_FORMAT``.
    16491649
     1650.. setting:: SIGNING_BACKEND
     1651
     1652SIGNING_BACKEND
     1653---------------
     1654
     1655.. versionadded:: 1.4
     1656
     1657Default: 'django.core.signing.TimestampSigner'
     1658
     1659The backend used for signing cookies and other data.
     1660
     1661See also the :ref:`topics-signing` documentation.
     1662
    16501663.. setting:: SITE_ID
    16511664
    16521665SITE_ID
  • docs/topics/http/shortcuts.txt

    diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt
    index 3a43fe1..37ec98e 100644
    a b This example is equivalent to::  
    8989``render_to_response``
    9090======================
    9191
    92 .. function:: render_to_response(template_name[, dictionary][, context_instance][, mimetype])
     92.. function:: render_to_response(template[, dictionary][, context_instance][, mimetype])
    9393
    9494   Renders a given template with a given context dictionary and returns an
    9595   :class:`~django.http.HttpResponse` object with that rendered text.
    This example is equivalent to::  
    9797Required arguments
    9898------------------
    9999
    100 ``template_name``
     100``template``
    101101    The full name of a template to use or sequence of template names. If a
    102102    sequence is given, the first template that exists will be used. See the
    103103    :ref:`template loader documentation <ref-templates-api-the-python-api>`
  • docs/topics/http/views.txt

    diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt
    index cfdd008..562be53 100644
    a b of a Web page, or a redirect, or a 404 error, or an XML document, or an image .  
    88. . or anything, really. The view itself contains whatever arbitrary logic is
    99necessary to return that response. This code can live anywhere you want, as long
    1010as it's on your Python path. There's no other requirement--no "magic," so to
    11 speak. For the sake of putting the code *somewhere*, the convention is to
    12 put views in a file called ``views.py``, placed in your project or
    13 application directory.
     11speak. For the sake of putting the code *somewhere*, let's create a file called
     12``views.py`` in the ``mysite`` directory, which you created in the previous
     13chapter.
    1414
    1515A simple view
    1616=============
    Let's step through this code one line at a time:  
    4747      exceptions, but we'll get to those later.)
    4848
    4949.. admonition:: Django's Time Zone
    50 
     50   
    5151    Django includes a ``TIME_ZONE`` setting that defaults to
    5252    ``America/Chicago``. This probably isn't where you live, so you might want
    5353    to change it in your settings file.
  • docs/topics/index.txt

    diff --git a/docs/topics/index.txt b/docs/topics/index.txt
    index 49a03be..84f9e9f 100644
    a b Introductions to all the key parts of Django you'll need to know:  
    1818   auth
    1919   cache
    2020   conditional-view-processing
     21   signing
    2122   email
    2223   i18n/index
    2324   logging
  • new file docs/topics/signing.txt

    diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt
    new file mode 100644
    index 0000000..8947212
    - +  
     1.. _topics-signing:
     2
     3=====================
     4Cryptographic signing
     5=====================
     6
     7.. module:: django.core.signing
     8   :synopsis: Django's signing framework.
     9
     10.. versionadded:: 1.4
     11
     12The golden rule of Web application security is to never trust data from
     13untrusted sources. Sometimes it can be useful to pass data through an
     14untrusted medium. Cryptographically signed values can be passed through an
     15untrusted channel safe in the knowledge that any tampering will be detected.
     16
     17Django provides both a low-level API for signing values and a high-level API
     18for setting and reading signed cookies, one of the most common uses of
     19signing in Web applications.
     20
     21You may also find signing useful for the following:
     22
     23    * Generating "recover my account" URLs for sending to users who have
     24      lost their password.
     25
     26    * Ensuring data stored in hidden form fields has not been tampered with.
     27
     28    * Generating one-time secret URLs for allowing temporary access to a
     29      protected resource, for example a downloadable file that a user has
     30      paid for.
     31
     32Protecting the SECRET_KEY
     33=========================
     34
     35When you create a new Django project using :djadmin:`startproject`, the
     36``settings.py`` file it generates automatically gets a random
     37:setting:`SECRET_KEY` value. This value is the key to securing signed
     38data -- it is vital you keep this secure, or attackers could use it to
     39generate their own signed values.
     40
     41Using the low-level API
     42=======================
     43
     44.. class:: Signer
     45
     46Django's signing methods live in the ``django.core.signing`` module.
     47To sign a value, first instantiate a ``Signer`` instance::
     48
     49    >>> from django.core.signing import Signer
     50    >>> signer = Signer()
     51    >>> value = signer.sign('My string')
     52    >>> value
     53    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
     54
     55The signature is appended to the end of the string, following the colon.
     56You can retrieve the original value using the ``unsign`` method::
     57
     58    >>> original = signer.unsign(value)
     59    >>> original
     60    u'My string'
     61
     62If the signature or value have been altered in any way, a
     63``django.core.signing.BadSigature`` exception will be raised::
     64
     65    >>> value += 'm'
     66    >>> try:
     67    ...    original = signer.unsign(value)
     68    ... except signing.BadSignature:
     69    ...    print "Tampering detected!"
     70
     71By default, the ``Signer`` class uses the :setting:`SECRET_KEY` setting to
     72generate signatures. You can use a different secret by passing it to the
     73``Signer`` constructor::
     74
     75    >>> signer = Signer('my-other-secret')
     76    >>> value = signer.sign('My string')
     77    >>> value
     78    'My string:EkfQJafvGyiofrdGnuthdxImIJw'
     79
     80Using the salt argument
     81-----------------------
     82
     83If you do not wish to use the same key for every signing operation in your
     84application, you can use the optional ``salt`` argument to the ``Signer``
     85class to further strengthen your :setting:`SECRET_KEY` against brute force
     86attacks. Using a salt will cause a new key to be derived from both the salt
     87and your :setting:`SECRET_KEY`::
     88
     89    >>> signer = Signer()
     90    >>> signer.sign('My string')
     91    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
     92    >>> signer = Signer(salt='extra')
     93    >>> signer.sign('My string')
     94    'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
     95    >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
     96    u'My string'
     97
     98Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
     99secret.
     100
     101Verifying timestamped values
     102----------------------------
     103
     104.. class:: TimestampSigner
     105
     106``TimestampSigner`` is a subclass of :class:`~Signer` that appends a signed
     107timestamp to the value. This allows you to confirm that a signed value was
     108created within a specified period of time::
     109
     110    >>> from django.core.signing import TimestampSigner
     111    >>> signer = TimestampSigner()
     112    >>> value = signer.sign('hello')
     113    >>> value
     114    'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
     115    >>> signer.unsign(value)
     116    u'hello'
     117    >>> signer.unsign(value, max_age=10)
     118    ...
     119    SignatureExpired: Signature age 15.5289158821 > 10 seconds
     120    >>> signer.unsign(value, max_age=20)
     121    u'hello'
     122
     123Protecting complex data structures
     124----------------------------------
     125
     126If you wish to protect a list, tuple or dictionary you can do so using the
     127signing module's dumps and loads functions. These imitate Python's pickle
     128module, but uses JSON serialization under the hood. JSON ensures that even
     129if your :setting:`SECRET_KEY` is stolen an attacker will not be able to
     130execute arbitrary commands by exploiting the pickle format.::
     131
     132    >>> from django.core import signing
     133    >>> value = signing.dumps({"foo": "bar"})
     134    >>> value
     135    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
     136    >>> signing.loads(value)
     137    {'foo': 'bar'}
  • docs/topics/testing.txt

    diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
    index 07049fd..36045d2 100644
    a b this use case Django provides a standard `Python context manager`_  
    13851385            response = self.client.get('/sekrit/')
    13861386            self.assertRedirects(response, '/accounts/login/?next=/sekrit/')
    13871387
    1388             # Then override the LOGIN_URL setting
     1388            # Then override the LOGING_URL setting
    13891389            with self.settings(LOGIN_URL='/other/login/'):
    13901390                response = self.client.get('/sekrit/')
    13911391                self.assertRedirects(response, '/other/login/?next=/sekrit/')
    decorate the class::  
    14381438.. note::
    14391439
    14401440    When overriding settings make sure to also handle the cases in which
    1441     Django or your app's code uses a cache or similar feature that retains
     1441    Django or your app's code use a cache or another feature that retain
    14421442    state even if the setting is changed. Django provides the
    14431443    :data:`django.test.signals.setting_changed` signal to connect cleanup
    1444     and other state-resetting callbacks to.
     1444    and other state resetting callbacks to.
    14451445
    14461446.. _`Python context manager`: http://www.python.org/dev/peps/pep-0343/
    14471447.. _`decorator`: http://www.python.org/dev/peps/pep-0318/
  • new file tests/regressiontests/signed_cookies_tests/models.py

    diff --git a/tests/regressiontests/signed_cookies_tests/__init__.py b/tests/regressiontests/signed_cookies_tests/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/signed_cookies_tests/models.py b/tests/regressiontests/signed_cookies_tests/models.py
    new file mode 100644
    index 0000000..71abcc5
    - +  
     1# models.py file for tests to run.
  • new file tests/regressiontests/signed_cookies_tests/tests.py

    diff --git a/tests/regressiontests/signed_cookies_tests/tests.py b/tests/regressiontests/signed_cookies_tests/tests.py
    new file mode 100644
    index 0000000..c28892a
    - +  
     1import time
     2
     3from django.core import signing
     4from django.http import HttpRequest, HttpResponse
     5from django.test import TestCase
     6
     7class SignedCookieTest(TestCase):
     8
     9    def test_can_set_and_read_signed_cookies(self):
     10        response = HttpResponse()
     11        response.set_signed_cookie('c', 'hello')
     12        self.assertIn('c', response.cookies)
     13        self.assertTrue(response.cookies['c'].value.startswith('hello:'))
     14        request = HttpRequest()
     15        request.COOKIES['c'] = response.cookies['c'].value
     16        value = request.get_signed_cookie('c')
     17        self.assertEqual(value, u'hello')
     18
     19    def test_can_use_salt(self):
     20        response = HttpResponse()
     21        response.set_signed_cookie('a', 'hello', salt='one')
     22        request = HttpRequest()
     23        request.COOKIES['a'] = response.cookies['a'].value
     24        value = request.get_signed_cookie('a', salt='one')
     25        self.assertEqual(value, u'hello')
     26        self.assertRaises(signing.BadSignature,
     27            request.get_signed_cookie, 'a', salt='two')
     28
     29    def test_detects_tampering(self):
     30        response = HttpResponse()
     31        response.set_signed_cookie('c', 'hello')
     32        request = HttpRequest()
     33        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
     34        self.assertRaises(signing.BadSignature,
     35            request.get_signed_cookie, 'c')
     36
     37    def test_default_argument_supresses_exceptions(self):
     38        response = HttpResponse()
     39        response.set_signed_cookie('c', 'hello')
     40        request = HttpRequest()
     41        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
     42        self.assertEqual(request.get_signed_cookie('c', default=None), None)
     43
     44    def test_max_age_argument(self):
     45        value = u'hello'
     46        _time = time.time
     47        time.time = lambda: 123456789
     48        try:
     49            response = HttpResponse()
     50            response.set_signed_cookie('c', value)
     51            request = HttpRequest()
     52            request.COOKIES['c'] = response.cookies['c'].value
     53            self.assertEqual(request.get_signed_cookie('c'), value)
     54
     55            time.time = lambda: 123456800
     56            self.assertEqual(request.get_signed_cookie('c', max_age=12), value)
     57            self.assertEqual(request.get_signed_cookie('c', max_age=11), value)
     58            self.assertRaises(signing.SignatureExpired,
     59                request.get_signed_cookie, 'c', max_age = 10)
     60        finally:
     61            time.time = _time
  • new file tests/regressiontests/signing/models.py

    diff --git a/tests/regressiontests/signing/__init__.py b/tests/regressiontests/signing/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/signing/models.py b/tests/regressiontests/signing/models.py
    new file mode 100644
    index 0000000..71abcc5
    - +  
     1# models.py file for tests to run.
  • new file tests/regressiontests/signing/tests.py

    diff --git a/tests/regressiontests/signing/tests.py b/tests/regressiontests/signing/tests.py
    new file mode 100644
    index 0000000..0b0cacf
    - +  
     1import time
     2
     3from django.core import signing
     4from django.test import TestCase
     5from django.utils.encoding import force_unicode
     6
     7class TestSigner(TestCase):
     8
     9    def test_signature(self):
     10        "signature() method should generate a signature"
     11        signer = signing.Signer('predictable-secret')
     12        signer2 = signing.Signer('predictable-secret2')
     13        for s in (
     14            'hello',
     15            '3098247:529:087:',
     16            u'\u2019'.encode('utf8'),
     17        ):
     18            self.assertEqual(
     19                signer.signature(s),
     20                signing.base64_hmac(signer.salt + 'signer', s,
     21                    'predictable-secret')
     22            )
     23            self.assertNotEqual(signer.signature(s), signer2.signature(s))
     24
     25    def test_signature_with_salt(self):
     26        "signature(value, salt=...) should work"
     27        signer = signing.Signer('predictable-secret', salt='extra-salt')
     28        self.assertEqual(
     29            signer.signature('hello'),
     30                signing.base64_hmac('extra-salt' + 'signer',
     31                'hello', 'predictable-secret'))
     32        self.assertNotEqual(
     33            signing.Signer('predictable-secret', salt='one').signature('hello'),
     34            signing.Signer('predictable-secret', salt='two').signature('hello'))
     35
     36    def test_sign_unsign(self):
     37        "sign/unsign should be reversible"
     38        signer = signing.Signer('predictable-secret')
     39        examples = (
     40            'q;wjmbk;wkmb',
     41            '3098247529087',
     42            '3098247:529:087:',
     43            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
     44            u'\u2019',
     45        )
     46        for example in examples:
     47            self.assertNotEqual(
     48                force_unicode(example), force_unicode(signer.sign(example)))
     49            self.assertEqual(example, signer.unsign(signer.sign(example)))
     50
     51    def unsign_detects_tampering(self):
     52        "unsign should raise an exception if the value has been tampered with"
     53        signer = signing.Signer('predictable-secret')
     54        value = 'Another string'
     55        signed_value = signer.sign(value)
     56        transforms = (
     57            lambda s: s.upper(),
     58            lambda s: s + 'a',
     59            lambda s: 'a' + s[1:],
     60            lambda s: s.replace(':', ''),
     61        )
     62        self.assertEqual(value, signer.unsign(signed_value))
     63        for transform in transforms:
     64            self.assertRaises(
     65                signing.BadSignature, signer.unsign, transform(signed_value))
     66
     67    def test_dumps_loads(self):
     68        "dumps and loads be reversible for any JSON serializable object"
     69        objects = (
     70            ['a', 'list'],
     71            'a string',
     72            u'a unicode string \u2019',
     73            {'a': 'dictionary'},
     74        )
     75        for o in objects:
     76            self.assertNotEqual(o, signing.dumps(o))
     77            self.assertEqual(o, signing.loads(signing.dumps(o)))
     78
     79    def test_decode_detects_tampering(self):
     80        "loads should raise exception for tampered objects"
     81        transforms = (
     82            lambda s: s.upper(),
     83            lambda s: s + 'a',
     84            lambda s: 'a' + s[1:],
     85            lambda s: s.replace(':', ''),
     86        )
     87        value = {
     88            'foo': 'bar',
     89            'baz': 1,
     90        }
     91        encoded = signing.dumps(value)
     92        self.assertEqual(value, signing.loads(encoded))
     93        for transform in transforms:
     94            self.assertRaises(
     95                signing.BadSignature, signing.loads, transform(encoded))
     96
     97class TestTimestampSigner(TestCase):
     98
     99    def test_timestamp_signer(self):
     100        value = u'hello'
     101        _time = time.time
     102        time.time = lambda: 123456789
     103        try:
     104            signer = signing.TimestampSigner('predictable-key')
     105            ts = signer.sign(value)
     106            self.assertNotEqual(ts,
     107                signing.Signer('predictable-key').sign(value))
     108
     109            self.assertEqual(signer.unsign(ts), value)
     110            time.time = lambda: 123456800
     111            self.assertEqual(signer.unsign(ts, max_age=12), value)
     112            self.assertEqual(signer.unsign(ts, max_age=11), value)
     113            self.assertRaises(
     114                signing.SignatureExpired, signer.unsign, ts, max_age=10)
     115        finally:
     116            time.time = _time
  • new file tests/regressiontests/utils/baseconv.py

    diff --git a/tests/regressiontests/utils/baseconv.py b/tests/regressiontests/utils/baseconv.py
    new file mode 100644
    index 0000000..75660d8
    - +  
     1from unittest import TestCase
     2from django.utils.baseconv import base2, base16, base36, base56, base62, base64, BaseConverter
     3
     4class TestBaseConv(TestCase):
     5
     6    def test_baseconv(self):
     7        nums = [-10 ** 10, 10 ** 10] + range(-100, 100)
     8        for converter in [base2, base16, base36, base56, base62, base64]:
     9            for i in nums:
     10                self.assertEqual(i, converter.decode(converter.encode(i)))
     11
     12    def test_base11(self):
     13        base11 = BaseConverter('0123456789-', sign='$')
     14        self.assertEqual(base11.encode(1234), '-22')
     15        self.assertEqual(base11.decode('-22'), 1234)
     16        self.assertEqual(base11.encode(-1234), '$-22')
     17        self.assertEqual(base11.decode('$-22'), -1234)
     18
     19    def test_base20(self):
     20        base20 = BaseConverter('0123456789abcdefghij')
     21        self.assertEqual(base20.encode(1234), '31e')
     22        self.assertEqual(base20.decode('31e'), 1234)
     23        self.assertEqual(base20.encode(-1234), '-31e')
     24        self.assertEqual(base20.decode('-31e'), -1234)
     25
     26    def test_base64(self):
     27        self.assertEqual(base64.encode(1234), 'JI')
     28        self.assertEqual(base64.decode('JI'), 1234)
     29        self.assertEqual(base64.encode(-1234), '$JI')
     30        self.assertEqual(base64.decode('$JI'), -1234)
     31
     32    def test_base7(self):
     33        base7 = BaseConverter('cjdhel3', sign='g')
     34        self.assertEqual(base7.encode(1234), 'hejd')
     35        self.assertEqual(base7.decode('hejd'), 1234)
     36        self.assertEqual(base7.encode(-1234), 'ghejd')
     37        self.assertEqual(base7.decode('ghejd'), -1234)
     38
     39    def test_exception(self):
     40        self.assertRaises(ValueError, BaseConverter, 'abc', sign='a')
     41        self.assertTrue(isinstance(BaseConverter('abc', sign='d'), BaseConverter))
  • tests/regressiontests/utils/tests.py

    diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
    index 5c4c060..2b61627 100644
    a b from timesince import *  
    1717from datastructures import *
    1818from tzinfo import *
    1919from datetime_safe import *
     20from baseconv import *
Back to Top