Skip to content

Commit

Permalink
Fix encryption issues (#161)
Browse files Browse the repository at this point in the history
* Fix encryption issues
* update donations gen
* make the change owner operation atomic
  • Loading branch information
tudoramariei authored Feb 2, 2024
1 parent c374da6 commit de89069
Show file tree
Hide file tree
Showing 16 changed files with 152 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ ALLOWED_HOSTS=localhost
APEX_DOMAIN=redirectioneaza.ro
SECRET_KEY="replace-this-example-key"
SENTRY_DSN=""
# key used for encrypting, data; has to be exactly 32 characters long
ECNRYPT_KEY="this-key-should-be-exactly-32-ch"

CORS_ALLOWED_ORIGINS=http://localhost:3000
CORS_ALLOW_ALL_ORIGINS=True
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ upd-sqlite: ## run the project with sqlite in detached mod
docker compose --profile sqlite3 up -d --build

up-mysql: ## run the project with mysql
docker compose --profile mysql up --build``
docker compose --profile mysql up --build

upd-mysql: ## run the project with mysql in detached mode
docker compose --profile mysql up -d --build
Expand Down
40 changes: 19 additions & 21 deletions backend/donations/management/commands/generate_donations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import random
import string
from typing import Any, Dict, List
from typing import List

from django.core.management import BaseCommand
from faker import Faker
Expand Down Expand Up @@ -33,45 +33,43 @@ def handle(self, *args, **options):
target_org = options.get("org", None)
self.stdout.write(f"Generating {total_donations} donations")

# create a list of all the NGOs
if not target_org:
ngos = list(Ngo.objects.filter(is_active=True))
else:
ngos = [Ngo.objects.get(id=target_org)]

generated_donations: List[Dict[str, Any]] = []
generated_donations: List[Donor] = []
while len(generated_donations) < total_donations:
# pick a random NGO
ngo = ngos[random.randint(0, len(ngos) - 1)]

# generate a random donor
donor = {
"ngo": ngo,
"first_name": fake.first_name(),
"last_name": fake.last_name(),
"initial": fake.first_name()[0],
"cnp": fake.ssn(),
"email": fake.email(),
"phone": fake.phone_number(),
"address": {
donor = Donor(
ngo=ngo,
first_name=fake.first_name(),
last_name=fake.last_name(),
initial=random.choice(string.ascii_uppercase),
email=fake.email(),
phone=fake.phone_number(),
city=fake.city(),
county=COUNTIES_CHOICES[random.randint(0, len(COUNTIES_CHOICES) - 1)][1],
income_type="wage",
)
donor.set_cnp(fake.ssn())
donor.set_address(
{
"street": fake.street_address(),
"number": fake.building_number(),
"bl": random.choice(["", random.randint(1, 20)]),
"sc": random.choice(["", random.choice(string.ascii_uppercase)]),
"et": random.choice(["", random.randint(1, 20)]),
"ap": random.choice(["", random.randint(1, 200)]),
},
"city": fake.city(),
"county": COUNTIES_CHOICES[random.randint(0, len(COUNTIES_CHOICES) - 1)][1],
"income_type": "wage",
}
}
)

# generate a random donation
generated_donations.append(donor)

# write to the database
self.stdout.write(self.style.SUCCESS("Writing to the database..."))

Donor.objects.bulk_create([Donor(**donation) for donation in generated_donations])
Donor.objects.bulk_create(generated_donations, batch_size=10)

self.stdout.write(self.style.SUCCESS("Done!"))
1 change: 1 addition & 0 deletions backend/donations/management/commands/generate_orgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ def handle(self, *args, **options):
"email": random.choice([owner_email, fake.email()]),
"website": fake.url(),
"is_active": create_valid or random.choice([True, False]),
"is_accepting_forms": create_valid or random.choice([True, False]),
}
try:
org = Ngo.objects.create(**organization_details)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.2.9 on 2024-02-01 17:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("donations", "0007_donor_address"),
]

operations = [
migrations.RemoveField(
model_name="donor",
name="address",
),
migrations.RemoveField(
model_name="donor",
name="cnp",
),
migrations.AddField(
model_name="donor",
name="encrypted_address",
field=models.TextField(blank=True, default="", verbose_name="address"),
),
migrations.AddField(
model_name="donor",
name="encrypted_cnp",
field=models.TextField(blank=True, default="", verbose_name="CNP"),
),
]
26 changes: 23 additions & 3 deletions backend/donations/models/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import hashlib
import json
from functools import partial

from django.conf import settings
Expand All @@ -8,7 +9,6 @@
from django.db.models.functions import Lower
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_cryptography.fields import encrypt


def select_public_storage():
Expand Down Expand Up @@ -216,7 +216,7 @@ class Donor(models.Model):
last_name = models.CharField(verbose_name=_("last name"), blank=True, null=False, default="", max_length=100)
initial = models.CharField(verbose_name=_("initials"), blank=True, null=False, default="", max_length=5)

cnp = encrypt(models.CharField(verbose_name=_("CNP"), blank=True, null=False, default="", max_length=13))
encrypted_cnp = models.TextField(verbose_name=_("CNP"), blank=True, null=False, default="")

city = models.CharField(
verbose_name=_("city"),
Expand All @@ -234,7 +234,7 @@ class Donor(models.Model):
max_length=100,
db_index=True,
)
address = models.JSONField(verbose_name=_("address"), blank=True, null=False, default=dict)
encrypted_address = models.TextField(verbose_name=_("address"), blank=True, null=False, default="")

# originally: tel
phone = models.CharField(verbose_name=_("telephone"), blank=True, null=False, default="", max_length=30)
Expand Down Expand Up @@ -285,3 +285,23 @@ class Meta:

def __str__(self):
return f"{self.ngo} {self.date_created} {self.email}"

def set_cnp(self, cnp: str):
self.encrypted_cnp = settings.FERNET_OBJECT.encrypt(cnp.encode()).decode()

def get_cnp(self) -> str:
return self.decrypt_cnp(self.encrypted_cnp)

def set_address(self, address: dict):
self.encrypted_address = settings.FERNET_OBJECT.encrypt(str(address).encode()).decode()

def get_address(self) -> dict:
return self.decrypt_address(self.encrypted_address)

@staticmethod
def decrypt_cnp(cnp: str) -> str:
return settings.FERNET_OBJECT.decrypt(cnp.encode()).decode()

@staticmethod
def decrypt_address(address):
return json.loads(settings.FERNET_OBJECT.decrypt(address.encode()).decode())
4 changes: 2 additions & 2 deletions backend/donations/views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def get(self, request, *args, **kwargs):
donations = Donor.objects.filter(date_created__gte=from_date).all()

stats = deepcopy(stats_dict)
stats["ngos"] = len(ngos) + stats_dict["ngos"]
stats["forms"] = len(donations) + stats_dict["forms"]
stats["ngos"] = ngos.count() + stats_dict["ngos"]
stats["forms"] = donations.count() + stats_dict["forms"]

self.add_data(stats, ngos, donations)

Expand Down
8 changes: 8 additions & 0 deletions backend/donations/views/my_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.core.validators import validate_email
from django.db import transaction
from django.db.models import Q, QuerySet
from django.shortcuts import redirect, render
from django.urls import reverse, reverse_lazy
Expand Down Expand Up @@ -56,12 +57,18 @@ def get(self, request, *args, **kwargs):
grouped_donors[index] = []
grouped_donors[index].append(donor)

now = timezone.now()
can_donate = not now.date() > settings.DONATIONS_LIMIT

context = {
"user": request.user,
"limit": settings.DONATIONS_LIMIT,
"ngo": user_ngo,
"donors": grouped_donors,
"counties": settings.FORM_COUNTIES,
"can_donate": can_donate,
"has_signed_form": user_ngo.is_accepting_forms,
"current_year": timezone.now().year,
}
return render(request, self.template_name, context)

Expand Down Expand Up @@ -158,6 +165,7 @@ def post(self, request, *args, **kwargs):
return redirect(reverse("association"))

@staticmethod
@transaction.atomic
def change_ngo_owner(ngo, new_ngo_owner):
try:
validate_email(new_ngo_owner)
Expand Down
26 changes: 16 additions & 10 deletions backend/donations/views/ngo.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,27 +321,33 @@ def get_post_value(arg, add_to_error_list=True):
donor = Donor(
first_name=donor_dict["first_name"],
last_name=donor_dict["last_name"],
initial=donor_dict["father"],
city=donor_dict["city"],
county=donor_dict["county"],
address={
"street": donor_dict["street"],
"number": donor_dict["number"],
"bl": donor_dict["bl"],
"sc": donor_dict["sc"],
"et": donor_dict["et"],
"ap": donor_dict["ap"],
},
email=donor_dict["email"],
phone=donor_dict["tel"],
email=donor_dict["email"],
is_anonymous=donor_dict["anonymous"],
two_years=two_years,
income_type=donor_dict["income"],
two_years=two_years,
# TODO:
# make a request to get geo ip data for this user
# geoip = self.get_geoip_data(),
ngo=ngo,
# TODO: 'filename' is unused
)

donor.set_cnp(donor_dict["cnp"])
donor.set_address(
{
"street": donor_dict["street"],
"number": donor_dict["number"],
"bl": donor_dict["bl"],
"sc": donor_dict["sc"],
"et": donor_dict["et"],
"ap": donor_dict["ap"],
}
)

donor.save()

pdf = create_pdf(donor_dict, ngo_data)
Expand Down
9 changes: 9 additions & 0 deletions backend/redirectioneaza/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
"""

import os
from base64 import urlsafe_b64encode
from copy import deepcopy
from datetime import date, datetime
from pathlib import Path

import environ
import sentry_sdk
from cryptography.fernet import Fernet
from django.utils import timezone
from localflavor.ro.ro_counties import COUNTIES_CHOICES

Expand Down Expand Up @@ -461,3 +463,10 @@

ZIP_ENDPOINT = env.str("ZIP_ENDPOINT")
ZIP_SECRET = env.str("ZIP_SECRET")


# encryption
ECNRYPT_KEY = env.str("ECNRYPT_KEY")
if len(ECNRYPT_KEY) != 32:
raise Exception("ECNRYPT_KEY must be exactly 32 characters long")
FERNET_OBJECT = Fernet(urlsafe_b64encode(ECNRYPT_KEY.encode("utf-8")))
30 changes: 16 additions & 14 deletions backend/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ build==1.0.3
certifi==2023.11.17
# via
# -r requirements.txt
# requests
# sentry-sdk
cffi==1.16.0
# via
Expand All @@ -33,36 +34,30 @@ chardet==5.2.0
# via
# -r requirements.txt
# reportlab
charset-normalizer==3.3.2
# via
# -r requirements.txt
# requests
click==8.1.7
# via
# black
# pip-tools
croniter==2.0.1
# via -r requirements.txt
cryptography==42.0.1
# via
# -r requirements.txt
# django-cryptography
cryptography==42.0.2
# via -r requirements.txt
cssselect2==0.7.0
# via
# -r requirements.txt
# svglib
django==4.2.9
# via
# -r requirements.txt
# django-appconf
# django-cryptography
# django-localflavor
# django-picklefield
# django-q2
# django-recaptcha
# django-storages
django-appconf==1.0.6
# via
# -r requirements.txt
# django-cryptography
django-cryptography==1.1
# via -r requirements.txt
django-environ==0.11.2
# via -r requirements.txt
django-localflavor==4.0
Expand All @@ -87,6 +82,10 @@ greenlet==3.0.3
# gevent
gunicorn==21.2.0
# via -r requirements.txt
idna==3.6
# via
# -r requirements.txt
# requests
jinja2==3.1.3
# via -r requirements.txt
jmespath==1.0.1
Expand Down Expand Up @@ -128,7 +127,7 @@ pycparser==2.21
# cffi
pymysql==1.1.0
# via -r requirements.txt
pypdf==4.0.0
pypdf==4.0.1
# via -r requirements.txt
pyproject-hooks==1.0.0
# via build
Expand Down Expand Up @@ -156,13 +155,15 @@ reportlab==4.0.9
# via
# -r requirements.txt
# svglib
requests==2.31.0
# via -r requirements.txt
ruff==0.1.14
# via -r requirements-dev.in
s3transfer==0.10.0
# via
# -r requirements.txt
# boto3
sentry-sdk==1.39.2
sentry-sdk==1.40.0
# via -r requirements.txt
six==1.16.0
# via
Expand All @@ -184,6 +185,7 @@ urllib3==2.0.7
# via
# -r requirements.txt
# botocore
# requests
# sentry-sdk
wcwidth==0.2.12
# via
Expand Down
Loading

0 comments on commit de89069

Please sign in to comment.