From 43c06c13dadcd084e1e019c6e6f76f3864615890 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 19 Sep 2024 11:40:53 +0200 Subject: [PATCH] Adds fields for Purchase --- .gitignore | 2 +- api/serializers.py | 17 +++++++ api/views.py | 6 +-- shared/cryptography.py | 51 +++++++++++++++++++ tournaments/admin.py | 2 +- ...e_purchase_id_alter_purchase_identifier.py | 22 ++++++++ .../0085_rename_identifier_purchase_id.py | 18 +++++++ tournaments/models/purchase.py | 5 +- tournaments/models/tournament.py | 11 ++-- tournaments/utils/cryptography.py | 20 -------- 10 files changed, 120 insertions(+), 34 deletions(-) create mode 100644 shared/cryptography.py create mode 100644 tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py create mode 100644 tournaments/migrations/0085_rename_identifier_purchase_id.py delete mode 100644 tournaments/utils/cryptography.py diff --git a/.gitignore b/.gitignore index 4e82b22..2f99ee3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ padelclub_backend/settings_local.py myenv/ -tournaments/config_local.py +shared/config_local.py # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/api/serializers.py b/api/serializers.py index 61ac1a0..1e2d6de 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -11,6 +11,21 @@ from django.core.mail import EmailMessage from django.contrib.sites.shortcuts import get_current_site from api.tokens import account_activation_token +from shared.cryptography import encryption_util + +class EncryptedUserField(serializers.Field): + def to_representation(self, value): + # Encrypt data when sending it out + return encryption_util.encrypt_aes_gcm(str(value.id)) + + def to_internal_value(self, data): + # Decrypt data when receiving it + decrypted_user_id = encryption_util.decrypt_aes_gcm(data) + user = CustomUser.objects.get(id=decrypted_user_id) + if decrypted_user_id is None: + raise serializers.ValidationError("Invalid encrypted data") + return user + class UserSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) @@ -152,6 +167,8 @@ class PlayerRegistrationSerializer(serializers.ModelSerializer): # ['id', 'team_registration_id', 'first_name', 'last_name', 'licence_id', 'rank', 'has_paid'] class PurchaseSerializer(serializers.ModelSerializer): + user = EncryptedUserField() + class Meta: model = Purchase fields = '__all__' diff --git a/api/views.py b/api/views.py index 6e19f66..acda60c 100644 --- a/api/views.py +++ b/api/views.py @@ -108,9 +108,9 @@ class PurchaseViewSet(viewsets.ModelViewSet): return [] def create(self, request, *args, **kwargs): - identifier = request.data.get('identifier') - if Purchase.objects.filter(identifier=identifier).exists(): - return Response({"detail": "This transaction identifier is already registered."}, status=status.HTTP_208_ALREADY_REPORTED) + id = request.data.get('id') + if Purchase.objects.filter(id=id).exists(): + return Response({"detail": "This transaction id is already registered."}, status=status.HTTP_208_ALREADY_REPORTED) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/shared/cryptography.py b/shared/cryptography.py new file mode 100644 index 0000000..1ca41d4 --- /dev/null +++ b/shared/cryptography.py @@ -0,0 +1,51 @@ +from Crypto.Cipher import AES +import base64 +import os +from .config_local import CRYPTO_KEY + +class EncryptionUtil: + + def __init__(self, key): + # In a real application, store this key securely (e.g., environment variables) + self.crypto_key = key + + def encrypt_aes_gcm(self, plaintext): + # Decode the base64 encoded key + key = base64.b64decode(self.crypto_key) + + # Generate a random 12-byte nonce + nonce = os.urandom(12) + + # Create the cipher object + cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) + + # Encrypt the plaintext + ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8')) + + # Combine nonce, ciphertext, and tag + encrypted_data = nonce + ciphertext + tag + + # Encode the result in base64 + encrypted_base64 = base64.b64encode(encrypted_data).decode('utf-8') + + return encrypted_base64 + + def decrypt_aes_gcm(self, encrypted_base64): + # Decode the base64 encoded data and key + encrypted_data = base64.b64decode(encrypted_base64) + key = base64.b64decode(self.crypto_key) + + # Extract the nonce, tag, and ciphertext from the combined encrypted data + nonce = encrypted_data[:12] # AES GCM nonce is 12 bytes + tag = encrypted_data[-16:] # AES GCM tag is 16 bytes + ciphertext = encrypted_data[12:-16] # Ciphertext is everything in between + + # Create the cipher object and decrypt the data + cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) + decrypted_data = cipher.decrypt_and_verify(ciphertext, tag) + + # Convert decrypted bytes to string (assuming UTF-8 encoding) + decrypted_text = decrypted_data.decode('utf-8') + return decrypted_text + +encryption_util = EncryptionUtil(CRYPTO_KEY) diff --git a/tournaments/admin.py b/tournaments/admin.py index bc7ee41..e0696cc 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -74,7 +74,7 @@ class ClubAdmin(admin.ModelAdmin): search_fields = ('name', 'acronym') class PurchaseAdmin(admin.ModelAdmin): - list_display = ['user', 'identifier', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date'] + list_display = ['id', 'user', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date'] list_filter = ['user'] class CourtAdmin(admin.ModelAdmin): diff --git a/tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py b/tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py new file mode 100644 index 0000000..36fcb03 --- /dev/null +++ b/tournaments/migrations/0084_remove_purchase_id_alter_purchase_identifier.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1 on 2024-09-18 08:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0083_purchase_expiration_date'), + ] + + operations = [ + migrations.RemoveField( + model_name='purchase', + name='id', + ), + migrations.AlterField( + model_name='purchase', + name='identifier', + field=models.BigIntegerField(primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/tournaments/migrations/0085_rename_identifier_purchase_id.py b/tournaments/migrations/0085_rename_identifier_purchase_id.py new file mode 100644 index 0000000..2de4b5b --- /dev/null +++ b/tournaments/migrations/0085_rename_identifier_purchase_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-09-18 08:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0084_remove_purchase_id_alter_purchase_identifier'), + ] + + operations = [ + migrations.RenameField( + model_name='purchase', + old_name='identifier', + new_name='id', + ), + ] diff --git a/tournaments/models/purchase.py b/tournaments/models/purchase.py index 8acca70..014a15e 100644 --- a/tournaments/models/purchase.py +++ b/tournaments/models/purchase.py @@ -3,9 +3,8 @@ import uuid from . import CustomUser class Purchase(models.Model): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) + id = models.BigIntegerField(primary_key=True, unique=True, editable=True) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) - identifier = models.BigIntegerField(unique=True) purchase_date = models.DateTimeField() product_id = models.CharField(max_length=100) quantity = models.IntegerField(null=True, blank=True) @@ -13,4 +12,4 @@ class Purchase(models.Model): expiration_date = models.DateTimeField(null=True, blank=True) def __str__(self): - return f"{self.identifier} > {self.product_id} - {self.purchase_date} - {self.user.username}" + return f"{self.id} > {self.product_id} - {self.purchase_date} - {self.user.username}" diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 0b43c87..0ff7b6f 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -7,8 +7,7 @@ from . import Event, TournamentPayment, FederalMatchCategory, FederalCategory, F import uuid from django.utils import timezone, formats from datetime import datetime, timedelta -from .. import config_local -from ..utils.cryptography import decrypt_aes_gcm +from shared.cryptography import encryption_util from ..utils.extensions import plural_format class TeamSortingType(models.IntegerChoices): @@ -67,8 +66,8 @@ class Tournament(models.Model): return self.display_name() def is_canceled(self): - if self.local_id and config_local.CRYPTO_KEY: - decrypted = decrypt_aes_gcm(self.local_id, config_local.CRYPTO_KEY) + if self.local_id: + decrypted = encryption_util.decrypt_aes_gcm(self.local_id) value = int(decrypted[18]) if 0 <= value <= 4: return True @@ -78,8 +77,8 @@ class Tournament(models.Model): return False def payment(self): - if self.global_id and config_local.CRYPTO_KEY: - decrypted = decrypt_aes_gcm(self.global_id, config_local.CRYPTO_KEY) + if self.global_id: + decrypted = encryption_util.decrypt_aes_gcm(self.global_id) value = int(decrypted[18]) return TournamentPayment(value) else: diff --git a/tournaments/utils/cryptography.py b/tournaments/utils/cryptography.py deleted file mode 100644 index 172c481..0000000 --- a/tournaments/utils/cryptography.py +++ /dev/null @@ -1,20 +0,0 @@ -from Crypto.Cipher import AES -import base64 - -def decrypt_aes_gcm(encrypted_base64, key_base64): - # Decode the base64 encoded data and key - encrypted_data = base64.b64decode(encrypted_base64) - key = base64.b64decode(key_base64) - - # Extract the nonce, tag, and ciphertext from the combined encrypted data - nonce = encrypted_data[:12] # AES GCM nonce is 12 bytes - tag = encrypted_data[-16:] # AES GCM tag is 16 bytes - ciphertext = encrypted_data[12:-16] # Ciphertext is everything in between - - # Create the cipher object and decrypt the data - cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) - decrypted_data = cipher.decrypt_and_verify(ciphertext, tag) - - # Convert decrypted bytes to string (assuming UTF-8 encoding) - decrypted_text = decrypted_data.decode('utf-8') - return decrypted_text