Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 185 additions & 13 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ channels = "^4.3.1"
channels-redis = "^4.3.0"
redis = "^6.4.0"
mysqlclient = "^2.2.4"
django-encrypted-model-fields = "^0.6.5"

[tool.poetry.group.dev.dependencies]
djlint = "^1.34.1"
pre-commit = "^3.6.0"
black = "^25.9.0"
isort = "^6.1.0"
flake8 = "^7.3.0"

[build-system]
requires = ["poetry-core"]
Expand All @@ -60,6 +64,6 @@ target-version = ['py312']
include = '\.pyi?$'

[tool.isort]
profile = "ruff"
profile = "black"
multi_line_output = 3
line_length = 120
119 changes: 119 additions & 0 deletions validate_encryption.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env python
"""
Quick validation script to demonstrate encryption is working.
This can be run to verify encryption is properly configured.
"""

import os
import sys

import django

# Setup Django environment
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings")
django.setup()

from cryptography.fernet import Fernet
from django.conf import settings
from django.contrib.auth.models import User

from web.models import Donation, Profile, WebRequest

print("=" * 70)
print("PERSONAL DATA ENCRYPTION VALIDATION")
print("=" * 70)
print()

# Check encryption key is configured
print("1. Checking encryption key configuration...")
try:
key = settings.FIELD_ENCRYPTION_KEY
if isinstance(key, str):
key = key.encode("utf-8")
fernet = Fernet(key)
print(" βœ… Encryption key is properly configured")
except Exception as e:
print(f" ❌ Error with encryption key: {e}")
sys.exit(1)

print()

# Test Profile encryption
print("2. Testing Profile field encryption...")
try:
# Get or create a test user
user, created = User.objects.get_or_create(username="encryption_test_user", defaults={"email": "test@example.com"})

# Update profile with test data
profile = user.profile
test_discord = "TestUser#1234"
profile.discord_username = test_discord
profile.save()

# Reload and verify
profile.refresh_from_db()
if profile.discord_username == test_discord:
print(" βœ… Profile encryption/decryption working")
else:
print(" ❌ Profile encryption failed")

# Cleanup
if created:
user.delete()

except Exception as e:
print(f" ❌ Error testing profile: {e}")

print()

# Test WebRequest encryption
print("3. Testing WebRequest IP address encryption...")
try:
test_ip = "192.168.1.100"
webrequest = WebRequest.objects.create(ip_address=test_ip, user="test", path="/test")

webrequest.refresh_from_db()
if webrequest.ip_address == test_ip:
print(" βœ… WebRequest IP encryption/decryption working")
else:
print(" ❌ WebRequest encryption failed")

webrequest.delete()

except Exception as e:
print(f" ❌ Error testing WebRequest: {e}")

print()

# Test Donation encryption
print("4. Testing Donation email encryption...")
try:
test_email = "donor@example.com"
donation = Donation.objects.create(email=test_email, amount=50.00, donation_type="one_time")

donation.refresh_from_db()
if donation.email == test_email:
print(" βœ… Donation email encryption/decryption working")
else:
print(" ❌ Donation encryption failed")

donation.delete()

except Exception as e:
print(f" ❌ Error testing Donation: {e}")

print()
print("=" * 70)
print("VALIDATION COMPLETE")
print("=" * 70)
print()
print("Encrypted fields:")
print(" β€’ Profile: discord_username, slack_username, github_username, stripe_account_id")
print(" β€’ WebRequest: ip_address")
print(" β€’ Donation: email")
print(" β€’ Order: shipping_address")
print(" β€’ FeatureVote: ip_address")
print()
print("All personal data is now encrypted at rest! πŸ”’")
print()
97 changes: 97 additions & 0 deletions web/encryption.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
Encryption utilities for personal data fields.

This module provides encrypted field types for sensitive personal data.
The encryption uses the FIELD_ENCRYPTION_KEY from settings (same as MESSAGE_ENCRYPTION_KEY).
"""

import json

from cryptography.fernet import Fernet
from django.conf import settings
from django.db import models
from encrypted_model_fields.fields import EncryptedCharField, EncryptedEmailField, EncryptedTextField

# Re-export the encrypted fields for easy import
CustomEncryptedCharField = EncryptedCharField
CustomEncryptedEmailField = EncryptedEmailField
CustomEncryptedTextField = EncryptedTextField


class CustomEncryptedJSONField(models.TextField):
"""
Encrypted JSONField that encrypts the entire JSON structure.
Uses the FIELD_ENCRYPTION_KEY from settings for encryption.
Stores encrypted data as text in the database.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fernet = None

def get_fernet(self):
"""Lazy initialization of Fernet cipher."""
if self.fernet is None:
key = settings.FIELD_ENCRYPTION_KEY
if isinstance(key, str):
key = key.encode("utf-8")
self.fernet = Fernet(key)
return self.fernet

def get_prep_value(self, value):
"""Encrypt the JSON data before saving to database."""
if value is None:
return None

# Convert to JSON string first
if isinstance(value, str):
json_str = value
else:
json_str = json.dumps(value)

# Encrypt the JSON string
fernet = self.get_fernet()
encrypted_data = fernet.encrypt(json_str.encode("utf-8"))
return encrypted_data.decode("utf-8")

def from_db_value(self, value, expression, connection):
"""Decrypt the JSON data when loading from database."""
if value is None:
return None

try:
# Try to decrypt - if it fails, assume it's plaintext (for migration)
fernet = self.get_fernet()
decrypted_data = fernet.decrypt(value.encode("utf-8"))
json_str = decrypted_data.decode("utf-8")
return json.loads(json_str)
except Exception:
# If decryption fails, treat as plaintext (backward compatibility)
try:
return json.loads(value)
except Exception:
return value

def to_python(self, value):
"""Convert the value to Python object."""
if value is None:
return None

if isinstance(value, dict):
return value

if isinstance(value, str):
try:
# Try to decrypt first
fernet = self.get_fernet()
decrypted_data = fernet.decrypt(value.encode("utf-8"))
json_str = decrypted_data.decode("utf-8")
return json.loads(json_str)
except Exception:
# If decryption fails, treat as plaintext
try:
return json.loads(value)
except Exception:
return value

return value
Loading