Projet

Général

Profil

0001-add-cryptor-connector-39431.patch

Thomas Noël, 04 mars 2020 11:34

Télécharger (22,9 ko)

Voir les différences:

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

 debian/control                                |   1 +
 passerelle/apps/cryptor/__init__.py           |   0
 .../apps/cryptor/migrations/0001_initial.py   |  50 ++++
 .../apps/cryptor/migrations/__init__.py       |   0
 passerelle/apps/cryptor/models.py             | 226 ++++++++++++++++++
 passerelle/settings.py                        |   1 +
 passerelle/static/css/style.css               |   4 +
 setup.py                                      |   1 +
 tests/test_cryptor.py                         | 182 ++++++++++++++
 9 files changed, 465 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
debian/control
35 35
    python-pyexcel-ods,
36 36
    python-pyexcel-xls,
37 37
    python-crypto,
38
    python-pycryptodome,
38 39
    python-feedparser,
39 40
    python-pdfrw,
40 41
    python-httplib2,
passerelle/apps/cryptor/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-03-04 10:25
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='Encryption RSA public key (PEM format)')),
37
                ('private_key', models.TextField(blank=True, validators=[passerelle.apps.cryptor.models.validate_rsa_key], verbose_name='Decryption 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 / 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 base64
18
import binascii
19
import json
20
import os
21
from uuid import uuid4
22

  
23
from Cryptodome.PublicKey import RSA
24
from Cryptodome.Random import get_random_bytes
25
from Cryptodome.Cipher import AES, PKCS1_OAEP
26

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

  
34
from passerelle.base.models import BaseResource
35
from passerelle.utils.api import endpoint
36
from passerelle.utils.files import atomic_write
37
from passerelle.utils.jsonresponse import APIError
38

  
39

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

  
59

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

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

  
78

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

  
94

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

  
108

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

  
115

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

  
127
    category = _('Misc')
128

  
129
    class Meta:
130
        verbose_name = _('Encryption / Decryption')
131

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

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

  
145

  
146
    @endpoint(name='file-encrypt', perm='can_encrypt',
147
              description=_('Encrypt a file'),
148
              post={
149
                  'description': _('File to encrypt'),
150
                  'request_body': {'schema': {'application/json': FILE_SCHEMA}}
151
              })
152
    def file_encrypt(self, request, post_data):
153
        if not self.public_key:
154
            raise APIError('missing public key')
155
        try:
156
            data = base64.b64decode(post_data['file']['content'])
157
        except (TypeError, binascii.Error):
158
            raise APIError('invalid base64 string', http_status=400)
159

  
160
        filename = post_data['file']['filename']
161
        content_type = post_data['file']['content_type']
162
        cfile = CryptedFile(resource=self, filename=filename, content_type=content_type)
163
        cfile.save()
164

  
165
        uuid = str(cfile.uuid)  # get string representation of UUID object
166

  
167
        if self.redirect_url_base:
168
            redirect_url_base = self.redirect_url_base
169
        else:
170
            redirect_url_base = request.build_absolute_uri('%sfile-decrypt/' % (
171
                self.get_absolute_url(),))
172
        redirect_url = urljoin(redirect_url_base, uuid)
173

  
174
        content_filename = self.get_filename(uuid, create=True)
175
        metadata_filename = '%s.json' % content_filename
176
        metadata = {
177
            'filename': cfile.filename,
178
            'content_type': cfile.content_type,
179
            'creation_timestamp': cfile.creation_timestamp.isoformat(),
180
            'redirect_url': redirect_url,
181
        }
182

  
183
        tmp_dir = os.path.join(default_storage.path(self.get_connector_slug()), self.slug, 'tmp')
184
        with atomic_write(content_filename, dir=tmp_dir) as fd:
185
            write_encrypt(fd, data, self.public_key)
186
        with atomic_write(metadata_filename, dir=tmp_dir, mode='w') as fd:
187
            json.dump(metadata, fd, indent=2)
188

  
189
        return {'data': metadata}
190

  
191
    @endpoint(name='file-decrypt', perm='can_decrypt',
192
              description=_('Decrypt a file'),
193
              pattern=r'(?P<uuid>[\w-]+)$',
194
              example_pattern='{uuid}/',
195
              parameters={
196
                  'uuid': {
197
                      'description': _('File identifier'),
198
                      'example_value': '12345678-abcd-4321-abcd-123456789012',
199
                  },
200
              })
201
    def file_decrypt(self, request, uuid):
202
        if not self.private_key:
203
            raise APIError('missing private key')
204
        content_filename = self.get_filename(uuid, create=False)
205
        metadata_filename = '%s.json' % content_filename
206
        if not os.path.exists(metadata_filename):
207
            raise APIError('unknown uuid', http_status=404)
208

  
209
        content = read_decrypt(open(content_filename, 'rb'), self.private_key)
210

  
211
        metadata = json.load(open(metadata_filename, 'r'))
212
        filename = metadata.get('filename')
213
        content_type = metadata.get('content_type')
214

  
215
        response = HttpResponse(content_type=content_type)
216
        response['Content-Disposition'] = 'inline; filename="%s"' % filename
217
        response.write(content)
218
        return response
219

  
220

  
221
class CryptedFile(models.Model):
222
    resource = models.ForeignKey(Cryptor, on_delete=models.PROTECT)
223
    uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
224
    filename = models.CharField(max_length=512, blank=False)
225
    content_type = models.CharField(max_length=128)
226
    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',
passerelle/static/css/style.css
177 177
	content: "\f1b9";  /* car */
178 178
}
179 179

  
180
li.connector.cryptor a::before {
181
	content: "\f023";  /* lock */
182
}
183

  
180 184
li.connector.status-down span.connector-name::after {
181 185
	font-family: FontAwesome;
182 186
	content: "\f00d"; /* times */
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
-