Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from setuptools import setup

# Metadata goes in setup.cfg. These are here for GitHub's dependency graph.
setup(name="secure-cookie", install_requires=["Werkzeug>0.15"])
setup(name="secure-cookie", install_requires=["Werkzeug>0.15", "itsdangerous>=1.1"])
62 changes: 62 additions & 0 deletions src/secure_cookie/cookie.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,15 @@ def application(request):
"""
import base64
import json as _json
import warnings
from datetime import datetime
from hashlib import sha1 as _default_hash
from hmac import new as hmac
from numbers import Number
from time import time

from itsdangerous import Signer
from itsdangerous.exc import BadSignature
from werkzeug.security import safe_str_cmp
from werkzeug.urls import url_quote_plus
from werkzeug.urls import url_unquote_plus
Expand Down Expand Up @@ -280,6 +283,19 @@ def serialize(self, expires=None):
if expires:
self["_expires"] = _date_to_unix(expires)

result = []
for key, value in sorted(self.items()):
result.append(
(
"{}={}".format(
url_quote_plus(key), self.quote(value).decode("ascii")
)
).encode("ascii")
)
signer = Signer(self.secret_key, digest_method=self.hash_method)
return signer.sign(b"&".join(result))

def _mac_serialize(self):
result = []
mac = hmac(self.secret_key, None, self.hash_method)

Expand Down Expand Up @@ -309,6 +325,52 @@ def unserialize(cls, string, secret_key):
if isinstance(secret_key, text_type):
secret_key = secret_key.encode("utf-8", "replace")

signer = Signer(secret_key, digest_method=cls.hash_method)
try:
serialized = signer.unsign(string)
except BadSignature:
return cls._mac_unserialize(string, secret_key)

items = {}
for item in serialized.split(b"&"):
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

if items is not None:
try:
for key, value in items.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 _mac_unserialize(cls, string, secret_key):
warnings.warn(
"Unserializing using the old scheme. This is deprecated and the fallback will be removed in version 2.0. Ensure cookies are re-serialized using the new ItsDangerous scheme.", # noqa
DeprecationWarning,
stacklevel=3,
)
try:
base64_hash, data = string.split(b"?", 1)
except (ValueError, IndexError):
Expand Down
18 changes: 17 additions & 1 deletion tests/test_cookie.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import json

import pytest
from werkzeug.http import parse_cookie
from werkzeug.wrappers import Request
from werkzeug.wrappers import Response
Expand All @@ -26,7 +27,8 @@ def test_basic_support():
assert not c2.should_save
assert c2 == c

c3 = SecureCookie.unserialize(s, b"wrong foo")
with pytest.warns(DeprecationWarning):
c3 = SecureCookie.unserialize(s, b"wrong foo")
assert not c3.modified
assert not c3.new
assert c3 == {}
Expand All @@ -51,6 +53,20 @@ def test_expire_support():
assert "x" not in c3


def test_hmac_serialization_support():
c = SecureCookie(secret_key=b"foo")
c["x"] = 42
s = c._mac_serialize()

with pytest.warns(DeprecationWarning):
c2 = SecureCookie.unserialize(s, b"foo")
assert c is not c2
assert not c2.new
assert not c2.modified
assert not c2.should_save
assert c2 == c


def test_wrapper_support():
req = Request.from_values()
resp = Response()
Expand Down