Projet

Général

Profil

0001-add-cryptor-connector-39431.patch

Thomas Noël, 24 février 2020 23:00

Télécharger (22,5 ko)

Voir les différences:

Subject: [PATCH] add cryptor connector (#39431)

 passerelle/apps/cryptor/__init__.py           |   0
 .../apps/cryptor/migrations/0001_initial.py   |  50 ++++
 .../apps/cryptor/migrations/__init__.py       |   0
 passerelle/apps/cryptor/models.py             | 243 ++++++++++++++++++
 passerelle/settings.py                        |   1 +
 setup.py                                      |   1 +
 tests/test_cryptor.py                         | 182 +++++++++++++
 7 files changed, 477 insertions(+)
 create mode 100644 passerelle/apps/cryptor/__init__.py
 create mode 100644 passerelle/apps/cryptor/migrations/0001_initial.py
 create mode 100644 passerelle/apps/cryptor/migrations/__init__.py
 create mode 100644 passerelle/apps/cryptor/models.py
 create mode 100644 tests/test_cryptor.py
passerelle/apps/cryptor/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-02-21 14:54
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7
import passerelle.apps.cryptor.models
8
import uuid
9

  
10

  
11
class Migration(migrations.Migration):
12

  
13
    initial = True
14

  
15
    dependencies = [
16
        ('base', '0016_auto_20191002_1443'),
17
    ]
18

  
19
    operations = [
20
        migrations.CreateModel(
21
            name='CryptedFile',
22
            fields=[
23
                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
24
                ('filename', models.CharField(max_length=512)),
25
                ('content_type', models.CharField(max_length=128)),
26
                ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
27
            ],
28
        ),
29
        migrations.CreateModel(
30
            name='Cryptor',
31
            fields=[
32
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
33
                ('title', models.CharField(max_length=50, verbose_name='Title')),
34
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
35
                ('description', models.TextField(verbose_name='Description')),
36
                ('public_key', models.TextField(blank=True, validators=[passerelle.apps.cryptor.models.validate_rsa_key], verbose_name='Encrypt RSA public key (PEM format)')),
37
                ('private_key', models.TextField(blank=True, validators=[passerelle.apps.cryptor.models.validate_rsa_key], verbose_name='Decrypt RSA private key (PEM format)')),
38
                ('redirect_url_base', models.URLField(blank=True, help_text='Base URL for redirect, empty for local', max_length=256, verbose_name='Base URL of decrypt system')),
39
                ('users', models.ManyToManyField(blank=True, related_name='_cryptor_users_+', related_query_name='+', to='base.ApiUser')),
40
            ],
41
            options={
42
                'verbose_name': 'Encryption and Decryption',
43
            },
44
        ),
45
        migrations.AddField(
46
            model_name='cryptedfile',
47
            name='resource',
48
            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cryptor.Cryptor'),
49
        ),
50
    ]
passerelle/apps/cryptor/models.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import contextlib
18
import base64
19
import binascii
20
import json
21
import os
22
import tempfile
23
from uuid import uuid4
24

  
25
from Cryptodome.PublicKey import RSA
26
from Cryptodome.Random import get_random_bytes
27
from Cryptodome.Cipher import AES, PKCS1_OAEP
28

  
29
from django.core.exceptions import ValidationError
30
from django.core.files.storage import default_storage
31
from django.db import models
32
from django.http import HttpResponse
33
from django.utils.six.moves.urllib_parse import urljoin
34
from django.utils.translation import ugettext_lazy as _
35

  
36
from passerelle.base.models import BaseResource
37
from passerelle.utils.api import endpoint
38
from passerelle.utils.jsonresponse import APIError
39

  
40

  
41
FILE_SCHEMA = {
42
    "$schema": "http://json-schema.org/draft-04/schema#",
43
    "title": "File to encrypt",
44
    "description": "",
45
    "type": "object",
46
    "required": ["file"],
47
    "properties": {
48
        "file": {
49
            "type": "object",
50
            "required": ["filename", "content_type", "content"],
51
            "properties": {
52
                "filename": {"type": "string"},
53
                "content_type": {"type": "string"},
54
                "content": {"type": "string"},
55
            }
56
        }
57
    }
58
}
59

  
60

  
61
# encrypt and decrypt are borrowed from
62
# https://www.pycryptodome.org/en/latest/src/examples.html#encrypt-data-with-rsa
63

  
64
def write_encrypt(out_file, data, key_pem):
65
    public_key = RSA.import_key(key_pem)
66
    session_key = get_random_bytes(16)
67
    # Encrypt the session key with the public RSA key
68
    cipher_rsa = PKCS1_OAEP.new(public_key)
69
    enc_session_key = cipher_rsa.encrypt(session_key)
70
    # Encrypt the data with the AES session key
71
    cipher_aes = AES.new(session_key, AES.MODE_EAX)
72
    ciphertext, tag = cipher_aes.encrypt_and_digest(data)
73
    # Store in out_file
74
    out_file.write(enc_session_key)
75
    out_file.write(cipher_aes.nonce)
76
    out_file.write(tag)
77
    out_file.write(ciphertext)
78

  
79

  
80
def read_decrypt(in_file, key_pem):
81
    private_key = RSA.import_key(key_pem)
82
    # Get crypt elements from in_file
83
    enc_session_key = in_file.read(private_key.size_in_bytes())
84
    nonce = in_file.read(16)
85
    tag = in_file.read(16)
86
    ciphertext = in_file.read()
87
    # Decrypt the session key with the private RSA key
88
    cipher_rsa = PKCS1_OAEP.new(private_key)
89
    session_key = cipher_rsa.decrypt(enc_session_key)
90
    # Decrypt the data with the AES session key
91
    cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
92
    decrypted = cipher_aes.decrypt_and_verify(ciphertext, tag)
93
    return decrypted
94

  
95

  
96
def makedir(dir_name):
97
    if not os.path.exists(dir_name):
98
        if default_storage.directory_permissions_mode:
99
            d_umask = os.umask(0)
100
            try:
101
                os.makedirs(dir_name, mode=default_storage.directory_permissions_mode)
102
            except OSError:
103
                pass
104
            finally:
105
                os.umask(d_umask)
106
        else:
107
            os.makedirs(dir_name)
108

  
109

  
110
def validate_rsa_key(key):
111
    try:
112
        RSA.import_key(key)
113
    except ValueError as ex:
114
        raise ValidationError(_('invalid RSA key (%s)') % ex)
115

  
116

  
117
class Cryptor(BaseResource):
118
    public_key = models.TextField(blank=True,
119
                                  verbose_name=_('Encrypt RSA public key (PEM format)'),
120
                                  validators=[validate_rsa_key])
121
    private_key = models.TextField(blank=True,
122
                                   verbose_name=_('Decrypt RSA private key (PEM format)'),
123
                                   validators=[validate_rsa_key])
124
    redirect_url_base = models.URLField(max_length=256, blank=True,
125
                                        verbose_name=_('Base URL of decrypt system'),
126
                                        help_text=_('Base URL for redirect, empty for local'))
127

  
128
    category = _('Misc')
129

  
130
    class Meta:
131
        verbose_name = _('Encrypt / Decrypt')
132

  
133
    def get_redirect_url_base_display(self):
134
        if self.redirect_url_base:
135
            return _('defined')  # don't show it, can be sensitive
136
        return _('this file-decrypt endpoint')
137

  
138
    def get_filename(self, uuid, create=False):
139
        dirname = os.path.join(default_storage.path(self.get_connector_slug()),
140
                               self.slug, uuid[0:2], uuid[2:4])
141
        if create:
142
            makedir(dirname)
143
        filename = os.path.join(dirname, uuid)
144
        return filename
145

  
146
    @contextlib.contextmanager
147
    def named_tempfile(self, *args, **kwargs):
148
        tmp_dir = os.path.join(default_storage.path(self.get_connector_slug()),
149
                               self.slug, 'tmp')
150
        makedir(tmp_dir)
151
        with tempfile.NamedTemporaryFile(dir=tmp_dir, suffix='.tmp', delete=False,
152
                                         *args, **kwargs) as tpf:
153
            yield tpf
154

  
155
    @endpoint(name='file-encrypt', perm='can_encrypt',
156
              description=_('Encrypt a file'),
157
              post={
158
                  'description': _('File to encrypt'),
159
                  'request_body': {'schema': {'application/json': FILE_SCHEMA}}
160
              })
161
    def file_encrypt(self, request, post_data):
162
        if not self.public_key:
163
            raise APIError('missing public key')
164
        try:
165
            data = base64.b64decode(post_data['file']['content'])
166
        except (TypeError, binascii.Error):
167
            raise APIError('invalid base64 string', http_status=400)
168

  
169
        filename = post_data['file']['filename']
170
        content_type = post_data['file']['content_type']
171
        cfile = CryptedFile(resource=self, filename=filename, content_type=content_type)
172
        cfile.save()
173

  
174
        uuid = str(cfile.uuid)
175

  
176
        if self.redirect_url_base:
177
            redirect_url_base = self.redirect_url_base
178
        else:
179
            redirect_url_base = request.build_absolute_uri('%sfile-decrypt/' % (
180
                self.get_absolute_url(),))
181
        redirect_url = urljoin(redirect_url_base, uuid)
182

  
183
        content_filename = self.get_filename(uuid, create=True)
184
        metadata_filename = '%s.json' % content_filename
185
        metadata = {
186
            'filename': cfile.filename,
187
            'content_type': cfile.content_type,
188
            'creation_timestamp': cfile.creation_timestamp.isoformat(),
189
            'redirect_url': redirect_url,
190
        }
191

  
192
        # atomic writes (tmp file + move)
193
        with self.named_tempfile() as tpf:
194
            write_encrypt(tpf, data, self.public_key)
195
            tpf.flush()
196
            os.fsync(tpf.file.fileno())
197
            tempfile_name = tpf.name
198
        os.rename(tempfile_name, content_filename)
199
        with self.named_tempfile(mode='w') as tpf:
200
            json.dump(metadata, tpf, indent=2)
201
            tpf.flush()
202
            os.fsync(tpf.file.fileno())
203
            tempfile_name = tpf.name
204
        os.rename(tempfile_name, metadata_filename)
205

  
206
        return {'data': metadata}
207

  
208
    @endpoint(name='file-decrypt', perm='can_decrypt',
209
              description=_('Decrypt a file'),
210
              pattern=r'(?P<uuid>[\w-]+)$',
211
              example_pattern='{uuid}/',
212
              parameters={
213
                  'uuid': {
214
                      'description': _('File identifier'),
215
                      'example_value': '12345678-abcd-4321-abcd-123456789012',
216
                  },
217
              })
218
    def file_decrypt(self, request, uuid):
219
        if not self.private_key:
220
            raise APIError('missing private key')
221
        content_filename = self.get_filename(uuid, create=False)
222
        metadata_filename = '%s.json' % content_filename
223
        if not os.path.exists(metadata_filename):
224
            raise APIError('unknown uuid', http_status=404)
225

  
226
        content = read_decrypt(open(content_filename, 'rb'), self.private_key)
227

  
228
        metadata = json.load(open(metadata_filename, 'r'))
229
        filename = metadata.get('filename')
230
        content_type = metadata.get('content_type')
231

  
232
        response = HttpResponse(content_type=content_type)
233
        response['Content-Disposition'] = 'inline; filename="%s"' % filename
234
        response.write(content)
235
        return response
236

  
237

  
238
class CryptedFile(models.Model):
239
    resource = models.ForeignKey(Cryptor, on_delete=models.PROTECT)
240
    uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
241
    filename = models.CharField(max_length=512, blank=False)
242
    content_type = models.CharField(max_length=128)
243
    creation_timestamp = models.DateTimeField(auto_now_add=True)
passerelle/settings.py
134 134
    'passerelle.apps.cityweb',
135 135
    'passerelle.apps.clicrdv',
136 136
    'passerelle.apps.cmis',
137
    'passerelle.apps.cryptor',
137 138
    'passerelle.apps.csvdatasource',
138 139
    'passerelle.apps.family',
139 140
    'passerelle.apps.feeds',
setup.py
104 104
            'jsonschema < 3.1',
105 105
            'zeep >= 3.2',
106 106
            'pycrypto',
107
            'pycryptodomex',
107 108
            'unidecode',
108 109
            'paramiko',
109 110
            'pdfrw',
tests/test_cryptor.py
1
# -*- coding: utf-8 -*-
2

  
3
import base64
4
import pytest
5

  
6
from django.contrib.contenttypes.models import ContentType
7
from django.core.exceptions import ValidationError
8
from django.utils.encoding import force_text
9

  
10
from passerelle.apps.cryptor.models import Cryptor, CryptedFile
11
from passerelle.base.models import ApiUser, AccessRight
12

  
13
import utils
14

  
15
PUBLIC_KEY = '''-----BEGIN CERTIFICATE-----
16
MIICyjCCAbKgAwIBAgIUQQzM2eFYF+LpUR3t2euAjZAwLCEwDQYJKoZIhvcNAQEL
17
BQAwDzENMAsGA1UEAwwEemVwbzAeFw0xOTA2MjIyMjMxMTVaFw0yOTA2MTkyMjMx
18
MTVaMA8xDTALBgNVBAMMBHplcG8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
19
AoIBAQC8BM3xDylze0bOm76IjidyhmFqJlnRvcpbeZVTM7r3qYOHqXFG7/GZL4yd
20
2bW5eL6TCUT3gLEgegGYGPwCkGPd1cq9h+2M7zvolGToRCvrBpxH5Vu6iRkEYyWN
21
yPhc02EUqYlz1FBBYRgyYHQ4jy0vsPH55g536OKLI4rVykszjwD9p0Kh+T2I1D9Z
22
bHyA6s0C8goUFZG7kvasFRIXTTPgUBGSnEN/VPSD5vM94Oj5K4t6P9GHd32Jo2O6
23
E5jAHbPR7I4nRBCJuxJEbfpmsaOuMkGQ5rMulk6LXvRZiT9UDCDem1k6uF6tJkZR
24
g+Uh5V4ZLCzP7sSHcRN2ftWZqAr7AgMBAAGjHjAcMAkGA1UdEwQCMAAwDwYDVR0R
25
BAgwBoIEemVwbzANBgkqhkiG9w0BAQsFAAOCAQEANo54TMbOR5Isd4lix87EM0N+
26
8kxovCLin/szK4+fGfnr0fCUswkhoZ3y6xnmXFX4S2IGLU8bTl+eQVg04VM/7Gg2
27
LvBTtiBmGESbiSaC1fS+DbPBjp1NBpfwbiQFEuQfRMS6ejeF1YMS8Oy9PpeujHDT
28
4cX0kPH2GkqOGtpAdKoOD5XzT3yu5IHv7/AWpl8LZ5hr3f1RbzfIzJ19oC5NDXIR
29
d1XsjuCCHFbCyfnDmuZuQaGCTCm9f4Z8Ynum6hmSSzNvy3YJLRKXEYYLB3+l2V+t
30
SCzQiVzqDeKZnAChvJRditvcKdG36TPHREyCPgkpUTmi0gEjDZDPyBmXhHXHYA==
31
-----END CERTIFICATE-----
32
'''
33
PRIVATE_KEY = '''-----BEGIN PRIVATE KEY-----
34
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8BM3xDylze0bO
35
m76IjidyhmFqJlnRvcpbeZVTM7r3qYOHqXFG7/GZL4yd2bW5eL6TCUT3gLEgegGY
36
GPwCkGPd1cq9h+2M7zvolGToRCvrBpxH5Vu6iRkEYyWNyPhc02EUqYlz1FBBYRgy
37
YHQ4jy0vsPH55g536OKLI4rVykszjwD9p0Kh+T2I1D9ZbHyA6s0C8goUFZG7kvas
38
FRIXTTPgUBGSnEN/VPSD5vM94Oj5K4t6P9GHd32Jo2O6E5jAHbPR7I4nRBCJuxJE
39
bfpmsaOuMkGQ5rMulk6LXvRZiT9UDCDem1k6uF6tJkZRg+Uh5V4ZLCzP7sSHcRN2
40
ftWZqAr7AgMBAAECggEAUQsdHhA0BNQZdEtLuJ7VwBbOfKvlQXQ2enGQ/RkqOUC3
41
Mk3GRxZ8JFSLnyrNmxHBy61OLgUp1F7iuwXh8tT8Rw21YzbpHTutrhXw3PEtoRPr
42
X04s2N3pi6uU72W2MITorrhZSDU3FsdcX7KVxh9pEcqKsvYIPIWEyQbb/EVDXwhC
43
K4TAsmmhsGaU/BB0WHkbzU5KlZqYQHfnoj5pmTLoeYj81Z8D5T9fceImcyuWLl8t
44
OHscbAjNpkS1X2vYNwMLhCAM7YhE63RjFo2G5fxx64wdgs+mllR0PCc3Nli+JPWE
45
LQ/KmVMPY6JH506WbZkEVgb9Gfuj6yASpu5zJxf3MQKBgQDwktbzKmK3zwSNwLdx
46
zZy4AnQb2PNKuryYts9R+nKqWnE5vJwekBIV9vsASK0Aoh4UWP9cUjQV3gZH3HIR
47
9xP8nJwpOLU7c/wPP81HUl7nf68MVP5OzemHY/78fYR8T3Jf/uFdW3AOQrCBjGIV
48
Zbjb5SisqCS2ODc7DSuqzliBMwKBgQDIEz7nhXyMilTlgiWw7iRm9RKWykVSjEuR
49
gxHAqPOkg/HxacfDYs/O+qteSdW/V7zzZ2QHMyr52BaL5sEz0lC7W3O1nfTS94VM
50
YK+MakRBImC5uZvpea+30vJbVLODnKskhsWF8aCqLi5KJvkR5aE0OySKXBur7Czq
51
X2/gK6jfGQKBgAbU1J/BE16O1V1FHLBxm0KqZyunRHlZxiM8BbUZPIpT2SU/kttX
52
UfwnsEb4yVjcQahoQpAXkX0RefIuc1rJPlsNA240Owk+KOkx8Z1V3HYMbScXfsU0
53
Ga6Li2EWG14AT4okTbf98bel8ycqmlprMg2kezwz5h76h674l8XY6DB7AoGBAKJL
54
Aka5gBtchpsZJEvOENc3Spnof60DQrVJVZgrNF+p7BMA1FsIhzsFGQdF603n9MyY
55
fIpelijOgROA3g2UN4qTF1wmQhbzUzxuXVgQR0dyhHWDOxZ7b+8z/QXawjcrWaQq
56
coVBSCtjhIb/8B/1XftJUk2tg4DE9nYzbkOwBq7ZAoGAPF1KSY9TzY7g8gmQzYuY
57
+CCHM3mR9PjSHhJS5VWTwGfw3zZpLptwxzmJAoi1DyrJVhBP17ADVitYK4GquiSB
58
z5aZ2AnUBc/xueO2ixL3ROOXYAeakrRAQ38G13ibYe2dQpv6/CTsZJOttnCErn54
59
3k2Y/kDV+c1uNbzyPiK2qaM=
60
-----END PRIVATE KEY-----
61
'''
62

  
63

  
64
@pytest.fixture
65
def cryptor(db):
66
    return Cryptor.objects.create(slug='test',
67
                                  private_key=PRIVATE_KEY,
68
                                  public_key=PUBLIC_KEY)
69

  
70

  
71
def test_cryptor_bad_keys(db):
72
    bad1 = Cryptor(slug='bad1', title='t', description='d', private_key='badkey')
73
    with pytest.raises(ValidationError) as e1:
74
        bad1.full_clean()
75
    assert e1.value.messages == ['invalid RSA key (RSA key format is not supported)']
76
    bad2 = Cryptor(slug='bad2', title='t', description='d', public_key='badkey')
77
    with pytest.raises(ValidationError) as e2:
78
        bad2.full_clean()
79
    assert e2.value.messages == ['invalid RSA key (RSA key format is not supported)']
80

  
81

  
82
def test_cryptor_restricted_access(app, cryptor):
83
    endpoint = utils.generic_endpoint_url('cryptor', 'file-encrypt', slug=cryptor.slug)
84
    assert endpoint == '/cryptor/test/file-encrypt'
85
    resp = app.get(endpoint, status=405)
86
    resp = app.post_json(endpoint, params={"foo": "bar"}, status=403)
87
    assert resp.json['err'] == 1
88
    assert 'PermissionDenied' in resp.json['err_class']
89

  
90
    endpoint = utils.generic_endpoint_url('cryptor', 'file-decrypt', slug=cryptor.slug) + '/uuid'
91
    assert endpoint == '/cryptor/test/file-decrypt/uuid'
92
    resp = app.post_json(endpoint, params={"foo": "bar"}, status=405)
93
    resp = app.get(endpoint, status=403)
94
    assert resp.json['err'] == 1
95
    assert 'PermissionDenied' in resp.json['err_class']
96

  
97

  
98
def test_cryptor_bad_requests(app, cryptor):
99
    # full opened access
100
    api = ApiUser.objects.create(username='all', keytype='', key='')
101
    obj_type = ContentType.objects.get_for_model(cryptor)
102
    AccessRight.objects.create(codename='can_encrypt', apiuser=api, resource_type=obj_type,
103
                               resource_pk=cryptor.pk)
104
    AccessRight.objects.create(codename='can_decrypt', apiuser=api, resource_type=obj_type,
105
                               resource_pk=cryptor.pk)
106

  
107
    endpoint = utils.generic_endpoint_url('cryptor', 'file-encrypt', slug=cryptor.slug)
108
    for bad_payload in ('error',
109
                        {"foo": "bar"},
110
                        ["not", "a", "dict"],
111
                        {"file": {"filename": "f", "content_type": "ct"}},
112
                        {"file": {"filename": "f", "content_type": "ct", "content": None}},
113
                        {"file": {"filename": "f", "content_type": "ct", "content": "NotBase64"}},
114
                        ):
115
        resp = app.post_json(endpoint, params=bad_payload, status=400)
116
        assert resp.json['err'] == 1
117

  
118
    endpoint = utils.generic_endpoint_url('cryptor', 'file-decrypt', slug=cryptor.slug)
119
    endpoint = endpoint + '/bad-uuid'
120
    resp = app.get(endpoint, status=404)
121

  
122

  
123
def test_cryptor_encrypt_decrypt(app, cryptor):
124
    api = ApiUser.objects.create(username='all', keytype='', key='')
125
    obj_type = ContentType.objects.get_for_model(cryptor)
126
    AccessRight.objects.create(codename='can_encrypt', apiuser=api, resource_type=obj_type,
127
                               resource_pk=cryptor.pk)
128
    AccessRight.objects.create(codename='can_decrypt', apiuser=api, resource_type=obj_type,
129
                               resource_pk=cryptor.pk)
130

  
131
    # encrypt
132
    endpoint = utils.generic_endpoint_url('cryptor', 'file-encrypt', slug=cryptor.slug)
133
    content = force_text(base64.b64encode(b'this is foo and bar'))
134
    payload = {"file": {"filename": "foo.txt", "content_type": "text/plain", "content": content}}
135

  
136
    resp = app.post_json(endpoint, params=payload, status=200)
137
    assert resp.json['err'] == 0
138
    assert CryptedFile.objects.count() == 1
139
    cfile = CryptedFile.objects.first()
140
    assert resp.json['data']['redirect_url'].endswith(
141
            '/cryptor/%s/file-decrypt/%s' % (cryptor.slug, cfile.uuid))
142
    cfile.delete()
143

  
144
    # encrypt with another redirect url
145
    cryptor.redirect_url_base = 'https://foo/bar/'
146
    cryptor.save()
147
    resp = app.post_json(endpoint, params=payload, status=200)
148
    assert resp.json['err'] == 0
149
    assert CryptedFile.objects.count() == 1
150
    cfile = CryptedFile.objects.first()
151
    assert resp.json['data']['redirect_url'] == 'https://foo/bar/%s' % cfile.uuid
152

  
153
    # remove public key = cannot encrypt
154
    cryptor.public_key = ''
155
    cryptor.save()
156
    resp = app.post_json(endpoint, params=payload, status=200)
157
    assert resp.json['err'] == 1
158
    assert resp.json['err_desc'] == 'missing public key'
159

  
160
    # decrypt
161
    endpoint = utils.generic_endpoint_url('cryptor', 'file-decrypt', slug=cryptor.slug)
162
    endpoint = endpoint + '/' + str(cfile.uuid)
163
    resp = app.get(endpoint, status=200)
164
    assert resp.content_type == 'text/plain'
165
    assert resp.content == b'this is foo and bar'
166
    assert resp.headers['Content-Disposition'] == 'inline; filename="foo.txt"'
167

  
168
    # remove all files does not remove data+metadata(json) files: decrypt still works
169
    CryptedFile.objects.all().delete()
170
    assert CryptedFile.objects.count() == 0
171
    resp = app.get(endpoint, status=200)
172
    assert resp.content_type == 'text/plain'
173
    assert resp.content == b'this is foo and bar'
174
    assert resp.headers['Content-Disposition'] == 'inline; filename="foo.txt"'
175

  
176
    # remove private key = cannot decrypt
177
    cryptor.private_key = ''
178
    cryptor.save()
179
    endpoint = endpoint + '/' + str(cfile.uuid)
180
    resp = app.get(endpoint, status=200)
181
    assert resp.json['err'] == 1
182
    assert resp.json['err_desc'] == 'missing private key'
0
-