From 0d9e8fd65ece010e5747f413bf84314b048a5bf6 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Wed, 3 Jun 2020 11:27:09 +0200
Subject: [PATCH] utils: use pycrytodomex, replace Crypto with Cryptodome
 (#43563)

---
 combo/apps/lingo/models.py       |  4 ++--
 combo/utils/crypto.py            |  6 +++---
 setup.py                         |  1 +
 tests/test_lingo_remote_regie.py | 10 +++++-----
 tests/test_utils.py              |  3 ++-
 5 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/combo/apps/lingo/models.py b/combo/apps/lingo/models.py
index cb0ca2a4..ce38c956 100644
--- a/combo/apps/lingo/models.py
+++ b/combo/apps/lingo/models.py
@@ -30,17 +30,17 @@ from django import template
 from django.conf import settings
 from django.db import models
 from django.forms import models as model_forms, Select
 from django.utils.translation import ugettext_lazy as _
 from django.utils import timezone, dateparse, six
 from django.core.mail import EmailMultiAlternatives
 from django.urls import reverse
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
-from django.utils.encoding import python_2_unicode_compatible
+from django.utils.encoding import force_bytes, python_2_unicode_compatible
 from django.utils.formats import localize
 from django.utils.http import urlencode
 from django.utils.six.moves.urllib import parse as urlparse
 from django.utils.timezone import make_aware, utc
 
 from django.contrib.auth.models import User
 from django.template.loader import render_to_string
 
@@ -454,17 +454,17 @@ class RemoteItem(object):
                    'autobilling': _('Autobilling has been set for this invoice.'),
                    'past-due-date': _('Due date is over.'),
                    }
         return settings.LINGO_NO_ONLINE_PAYMENT_REASONS.get(self.no_online_payment_reason,
                                 reasons.get(self.no_online_payment_reason))
 
     @property
     def crypto_id(self):
-        return aes_hex_encrypt(settings.SECRET_KEY, str(self.id))
+        return aes_hex_encrypt(settings.SECRET_KEY, force_bytes(str(self.id)))
 
 
 class Transaction(models.Model):
     regie = models.ForeignKey(Regie, on_delete=models.CASCADE, null=True)
     items = models.ManyToManyField(BasketItem, blank=True)
     remote_items = models.CharField(max_length=512)
     to_be_paid_remote_items = models.CharField(max_length=512, null=True)
     start_date = models.DateTimeField(auto_now_add=True)
diff --git a/combo/utils/crypto.py b/combo/utils/crypto.py
index 31875e0a..01620963 100644
--- a/combo/utils/crypto.py
+++ b/combo/utils/crypto.py
@@ -11,19 +11,19 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import binascii
 
-from Crypto.Cipher import AES
-from Crypto.Protocol.KDF import PBKDF2
-from Crypto import Random
+from Cryptodome.Cipher import AES
+from Cryptodome.Protocol.KDF import PBKDF2
+from Cryptodome import Random
 
 from django.utils import six
 from django.utils.encoding import force_text
 
 
 class DecryptionError(Exception):
     pass
 
diff --git a/setup.py b/setup.py
index b868970b..c94a39c0 100644
--- a/setup.py
+++ b/setup.py
@@ -159,16 +159,17 @@ setup(
         'XStatic_OpenSans',
         'XStatic_roboto-fontface>=0.5.0.0',
         'eopayment>=1.43',
         'python-dateutil',
         'djangorestframework>=3.3, <3.8',
         'django-ratelimit<3',
         'sorl-thumbnail',
         'Pillow',
+        'pycryptodomex',
         'pyproj',
         'pywebpush',
         'pygal',
         'lxml',
         ],
     zip_safe=False,
     cmdclass={
         'build': build,
diff --git a/tests/test_lingo_remote_regie.py b/tests/test_lingo_remote_regie.py
index b68d8fe6..e11ac578 100644
--- a/tests/test_lingo_remote_regie.py
+++ b/tests/test_lingo_remote_regie.py
@@ -8,17 +8,17 @@ from decimal import Decimal
 from requests.exceptions import ConnectionError
 
 from django.apps import apps
 from django.test.client import RequestFactory
 from django.test import override_settings
 from django.urls import reverse
 from django.conf import settings
 from django.core.management import call_command
-from django.utils.encoding import force_text
+from django.utils.encoding import force_bytes, force_text
 from django.utils.six.moves.urllib import parse as urlparse
 from django.utils.timezone import timedelta, now
 from django.contrib.auth.models import User
 
 from combo.utils import check_query, aes_hex_encrypt
 from combo.data.models import Page
 from combo.apps.lingo.models import (Regie, ActiveItems, ItemsHistory, SelfDeclaredInvoicePayment,
                                      Transaction, BasketItem, PaymentBackend)
@@ -215,17 +215,17 @@ def test_remote_regie_past_invoices_cell(mock_send, remote_regie):
     content = cell.render(context)
     assert content.strip() == ''
 
 
 @mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
 @mock.patch('combo.apps.lingo.models.requests.get')
 def test_anonymous_successful_item_payment(mock_get, mock_pay_invoice, app, remote_regie):
     assert remote_regie.is_remote() == True
-    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, 'F201601')
+    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, force_bytes('F201601'))
     # invoice with amount_paid
     invoices = copy.deepcopy(INVOICES)
     invoices[0]['amount'] = '100.00'
     invoices[0]['amount_paid'] = '23.45'
     mock_json = mock.Mock()
     mock_json.json.return_value = {'err': 0, 'data': invoices[0]}
     mock_get.return_value = mock_json
     mock_pay_invoice.return_value = mock.Mock(status_code=200)
@@ -289,17 +289,17 @@ def test_anonymous_successful_item_payment(mock_get, mock_pay_invoice, app, remo
     assert b_item.amount == Decimal(INVOICES[0]['amount'])
     assert b_item in trans.items.all()
 
     assert resp.status_code == 200
 
 @mock.patch('combo.apps.lingo.models.requests.get')
 def test_anonymous_item_payment_email_error(mock_get, app, remote_regie):
     assert remote_regie.is_remote() == True
-    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, 'F201601')
+    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, force_bytes('F201601'))
     mock_json = mock.Mock()
     mock_json.json.return_value = {'err': 0, 'data': INVOICES[0]}
     mock_get.return_value = mock_json
     resp = app.get('/lingo/item/%s/%s/' % (remote_regie.id, encrypt_id))
     form = resp.form
     resp = form.submit()
 
     assert resp.status_code == 302
@@ -358,17 +358,17 @@ def test_self_declared_invoice(mock_get, app, remote_regie):
 
 @mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
 @mock.patch('combo.apps.lingo.models.requests.get')
 @mock.patch('combo.apps.lingo.models.requests.post')
 def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app, remote_regie):
     page = Page(title='xxx', slug='active-remote-invoices-page', template_name='standard')
     page.save()
     assert remote_regie.is_remote()
-    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, 'F201601')
+    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, force_bytes('F201601'))
     mock_json = mock.Mock()
     mock_json.json.return_value = {'err': 0, 'data': INVOICES[0]}
     mock_get.return_value = mock_json
     mock_pay_invoice.return_value = mock.Mock(status_code=200)
     resp = app.get('/lingo/item/%s/%s/?page=%s' % (remote_regie.id, encrypt_id, page.pk))
     form = resp.form
 
     assert 'email' in form.fields
@@ -427,17 +427,17 @@ def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app,
 
 
 @mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
 @mock.patch('combo.apps.lingo.models.requests.get')
 def test_remote_invoice_successfull_payment_redirect(mock_get, mock_pay_invoice, app, remote_regie):
     page = Page(title='xxx', slug='active-remote-invoices-page', template_name='standard')
     page.save()
     assert remote_regie.is_remote()
-    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, 'F201601')
+    encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, force_bytes('F201601'))
     mock_json = mock.Mock()
     mock_json.json.return_value = {'err': 0, 'data': INVOICES[0]}
     mock_get.return_value = mock_json
     mock_pay_invoice.return_value = mock.Mock(status_code=200)
     resp = app.get('/lingo/item/%s/%s/?page=%s' % (remote_regie.id, encrypt_id, page.pk))
     form = resp.form
     assert form['next_url'].value == '/active-remote-invoices-page/'
     form['email'] = 'test@example.net'
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 356bc022..f5eebf9b 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,16 +2,17 @@ import pytest
 
 from combo.utils import (aes_hex_decrypt, aes_hex_encrypt, get_templated_url,
                          TemplateError)
 from django.conf import settings
 from django.test import override_settings
 from django.template import Context, RequestContext
 from django.test.client import RequestFactory
 from django.contrib.auth.models import AnonymousUser
+from django.utils.encoding import force_bytes
 
 
 class MockUser(object):
     email = 'foo=3@example.net'
     is_authenticated = True
 
     def __init__(self, samlized=True):
         self.samlized = samlized
@@ -20,17 +21,17 @@ class MockUser(object):
         if self.samlized:
             return 'r2&d2'
         return None
 
 
 def test_crypto_url():
     invoice_id = '12-1234'
     key = settings.SECRET_KEY
-    assert aes_hex_decrypt(key, aes_hex_encrypt(key, invoice_id)) == invoice_id
+    assert aes_hex_decrypt(key, aes_hex_encrypt(key, force_bytes(invoice_id))) == invoice_id
 
 
 def test_templated_url():
     assert get_templated_url('foobar') == 'foobar'
     assert get_templated_url('foo[]bar') == 'foo[]bar'
     assert get_templated_url('foo[bar') == 'foo[bar'
     assert get_templated_url('foo]bar') == 'foo]bar'
     assert get_templated_url('foo]bar[') == 'foo]bar['
-- 
2.26.2

