• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
4
5from __future__ import absolute_import, division, print_function
6
7import base64
8import calendar
9import json
10import os
11import time
12
13import iso8601
14
15import pytest
16
17import six
18
19from cryptography.fernet import Fernet, InvalidToken, MultiFernet
20from cryptography.hazmat.backends import default_backend
21from cryptography.hazmat.backends.interfaces import CipherBackend, HMACBackend
22from cryptography.hazmat.primitives.ciphers import algorithms, modes
23
24import cryptography_vectors
25
26
27def json_parametrize(keys, filename):
28    vector_file = cryptography_vectors.open_vector_file(
29        os.path.join("fernet", filename), "r"
30    )
31    with vector_file:
32        data = json.load(vector_file)
33        return pytest.mark.parametrize(
34            keys, [tuple([entry[k] for k in keys]) for entry in data]
35        )
36
37
38def test_default_backend():
39    f = Fernet(Fernet.generate_key())
40    assert f._backend is default_backend()
41
42
43@pytest.mark.requires_backend_interface(interface=CipherBackend)
44@pytest.mark.requires_backend_interface(interface=HMACBackend)
45@pytest.mark.supported(
46    only_if=lambda backend: backend.cipher_supported(
47        algorithms.AES(b"\x00" * 32), modes.CBC(b"\x00" * 16)
48    ),
49    skip_message="Does not support AES CBC",
50)
51class TestFernet(object):
52    @json_parametrize(
53        ("secret", "now", "iv", "src", "token"),
54        "generate.json",
55    )
56    def test_generate(self, secret, now, iv, src, token, backend):
57        f = Fernet(secret.encode("ascii"), backend=backend)
58        actual_token = f._encrypt_from_parts(
59            src.encode("ascii"),
60            calendar.timegm(iso8601.parse_date(now).utctimetuple()),
61            b"".join(map(six.int2byte, iv)),
62        )
63        assert actual_token == token.encode("ascii")
64
65    @json_parametrize(
66        ("secret", "now", "src", "ttl_sec", "token"),
67        "verify.json",
68    )
69    def test_verify(
70        self, secret, now, src, ttl_sec, token, backend, monkeypatch
71    ):
72        f = Fernet(secret.encode("ascii"), backend=backend)
73        current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple())
74        payload = f.decrypt_at_time(
75            token.encode("ascii"),
76            ttl=ttl_sec,
77            current_time=current_time,
78        )
79        assert payload == src.encode("ascii")
80        monkeypatch.setattr(time, "time", lambda: current_time)
81        payload = f.decrypt(token.encode("ascii"), ttl=ttl_sec)
82        assert payload == src.encode("ascii")
83
84    @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json")
85    def test_invalid(self, secret, token, now, ttl_sec, backend, monkeypatch):
86        f = Fernet(secret.encode("ascii"), backend=backend)
87        current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple())
88        with pytest.raises(InvalidToken):
89            f.decrypt_at_time(
90                token.encode("ascii"),
91                ttl=ttl_sec,
92                current_time=current_time,
93            )
94        monkeypatch.setattr(time, "time", lambda: current_time)
95        with pytest.raises(InvalidToken):
96            f.decrypt(token.encode("ascii"), ttl=ttl_sec)
97
98    def test_invalid_start_byte(self, backend):
99        f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
100        with pytest.raises(InvalidToken):
101            f.decrypt(base64.urlsafe_b64encode(b"\x81"))
102
103    def test_timestamp_too_short(self, backend):
104        f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
105        with pytest.raises(InvalidToken):
106            f.decrypt(base64.urlsafe_b64encode(b"\x80abc"))
107
108    def test_non_base64_token(self, backend):
109        f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
110        with pytest.raises(InvalidToken):
111            f.decrypt(b"\x00")
112
113    def test_unicode(self, backend):
114        f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
115        with pytest.raises(TypeError):
116            f.encrypt(u"")
117        with pytest.raises(TypeError):
118            f.decrypt(u"")
119
120    def test_timestamp_ignored_no_ttl(self, monkeypatch, backend):
121        f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
122        pt = b"encrypt me"
123        token = f.encrypt(pt)
124        ts = "1985-10-26T01:20:01-07:00"
125        current_time = calendar.timegm(iso8601.parse_date(ts).utctimetuple())
126        monkeypatch.setattr(time, "time", lambda: current_time)
127        assert f.decrypt(token, ttl=None) == pt
128
129    def test_ttl_required_in_decrypt_at_time(self, monkeypatch, backend):
130        f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
131        pt = b"encrypt me"
132        token = f.encrypt(pt)
133        with pytest.raises(ValueError):
134            f.decrypt_at_time(token, ttl=None, current_time=int(time.time()))
135
136    @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"])
137    def test_roundtrips(self, message, backend):
138        f = Fernet(Fernet.generate_key(), backend=backend)
139        assert f.decrypt(f.encrypt(message)) == message
140
141    def test_bad_key(self, backend):
142        with pytest.raises(ValueError):
143            Fernet(base64.urlsafe_b64encode(b"abc"), backend=backend)
144
145    def test_extract_timestamp(self, monkeypatch, backend):
146        f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
147        current_time = 1526138327
148        token = f.encrypt_at_time(b"encrypt me", current_time)
149        assert f.extract_timestamp(token) == current_time
150        with pytest.raises(InvalidToken):
151            f.extract_timestamp(b"nonsensetoken")
152
153
154@pytest.mark.requires_backend_interface(interface=CipherBackend)
155@pytest.mark.requires_backend_interface(interface=HMACBackend)
156@pytest.mark.supported(
157    only_if=lambda backend: backend.cipher_supported(
158        algorithms.AES(b"\x00" * 32), modes.CBC(b"\x00" * 16)
159    ),
160    skip_message="Does not support AES CBC",
161)
162class TestMultiFernet(object):
163    def test_encrypt(self, backend):
164        f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
165        f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend)
166        f = MultiFernet([f1, f2])
167
168        assert f1.decrypt(f.encrypt(b"abc")) == b"abc"
169
170    def test_decrypt(self, backend):
171        f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
172        f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend)
173        f = MultiFernet([f1, f2])
174
175        assert f.decrypt(f1.encrypt(b"abc")) == b"abc"
176        assert f.decrypt(f2.encrypt(b"abc")) == b"abc"
177
178        with pytest.raises(InvalidToken):
179            f.decrypt(b"\x00" * 16)
180
181    def test_decrypt_at_time(self, backend):
182        f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
183        f = MultiFernet([f1])
184        pt = b"encrypt me"
185        token = f.encrypt_at_time(pt, current_time=100)
186        assert f.decrypt_at_time(token, ttl=1, current_time=100) == pt
187        with pytest.raises(InvalidToken):
188            f.decrypt_at_time(token, ttl=1, current_time=102)
189        with pytest.raises(ValueError):
190            f.decrypt_at_time(token, ttl=None, current_time=100)
191
192    def test_no_fernets(self, backend):
193        with pytest.raises(ValueError):
194            MultiFernet([])
195
196    def test_non_iterable_argument(self, backend):
197        with pytest.raises(TypeError):
198            MultiFernet(None)
199
200    def test_rotate(self, backend):
201        f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
202        f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend)
203
204        mf1 = MultiFernet([f1])
205        mf2 = MultiFernet([f2, f1])
206
207        plaintext = b"abc"
208        mf1_ciphertext = mf1.encrypt(plaintext)
209
210        assert mf2.decrypt(mf1_ciphertext) == plaintext
211
212        rotated = mf2.rotate(mf1_ciphertext)
213
214        assert rotated != mf1_ciphertext
215        assert mf2.decrypt(rotated) == plaintext
216
217        with pytest.raises(InvalidToken):
218            mf1.decrypt(rotated)
219
220    def test_rotate_preserves_timestamp(self, backend, monkeypatch):
221        f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
222        f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend)
223
224        mf1 = MultiFernet([f1])
225        mf2 = MultiFernet([f2, f1])
226
227        plaintext = b"abc"
228        original_time = int(time.time()) - 5 * 60
229        mf1_ciphertext = mf1.encrypt_at_time(plaintext, original_time)
230
231        rotated_time, _ = Fernet._get_unverified_token_data(
232            mf2.rotate(mf1_ciphertext)
233        )
234
235        assert int(time.time()) != rotated_time
236        assert original_time == rotated_time
237
238    def test_rotate_decrypt_no_shared_keys(self, backend):
239        f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend)
240        f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend)
241
242        mf1 = MultiFernet([f1])
243        mf2 = MultiFernet([f2])
244
245        with pytest.raises(InvalidToken):
246            mf2.rotate(mf1.encrypt(b"abc"))
247