From 45b77421042dc9080578327768a9d7e8ec36c38c Mon Sep 17 00:00:00 2001 From: KafetzisThomas Date: Wed, 8 Jan 2025 20:13:41 +0200 Subject: [PATCH] Implement per-user encryption for sensitive data --- passmanager/models.py | 61 ++++++++++++++++++- passmanager/views.py | 59 +++--------------- .../0007_customuser_encryption_salt.py | 18 ++++++ users/models.py | 10 ++- 4 files changed, 95 insertions(+), 53 deletions(-) create mode 100644 users/migrations/0007_customuser_encryption_salt.py diff --git a/passmanager/models.py b/passmanager/models.py index e043874..f65cb86 100644 --- a/passmanager/models.py +++ b/passmanager/models.py @@ -1,5 +1,24 @@ -from django.db import models +import base64 +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.fernet import Fernet from django.conf import settings +from django.db import models + + +def derive_key_from_master_password(master_password, salt): + """ + Derive an encryption key from the user's master password, + and encryption salt. + """ + kdf = PBKDF2HMAC( + algorithm=SHA256(), + length=32, + salt=salt, + iterations=100_000, + ) + key = base64.urlsafe_b64encode(kdf.derive(master_password.encode())) + return key class Item(models.Model): @@ -12,5 +31,45 @@ class Item(models.Model): last_modified = models.DateTimeField(auto_now=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + def encrypt_field(self, key, value): + """ + Encrypt field value using the given key. + """ + return Fernet(key).encrypt(value.encode()).decode() + + def decrypt_field(self, key, value): + """ + Decrypt field value using the given key. + """ + return Fernet(key).decrypt(value.encode()).decode() + + def get_key(self): + """ + Derive the encryption key using owner's master password, + and their encryption salt. + """ + salt = self.owner.encryption_salt.encode() + return derive_key_from_master_password(self.owner.password, salt) + + def save(self, *args, **kwargs): + """ + Encrypt sensitive fields before saving. + """ + key = self.get_key() + self.username = self.encrypt_field(key, self.username) + self.password = self.encrypt_field(key, self.password) + self.notes = self.encrypt_field(key, self.notes) + super().save(*args, **kwargs) + print(key) + + def decrypt_sensitive_fields(self): + """ + Decrypt sensitive fields for display. + """ + key = self.get_key() + self.username = self.decrypt_field(key, self.username) + self.password = self.decrypt_field(key, self.password) + self.notes = self.decrypt_field(key, self.notes) + def __str__(self): return self.name diff --git a/passmanager/views.py b/passmanager/views.py index 17d86d2..9af561e 100644 --- a/passmanager/views.py +++ b/passmanager/views.py @@ -39,30 +39,16 @@ def new_item(request): form = ItemForm(data=request.POST) obj = form.save(commit=False) - url_entry = obj.url username_entry = obj.username - password_entry = obj.password notes_entry = obj.notes if form.is_valid(): action = request.POST.get("action", "value") if action == "save": - obj.username = encrypt( - username_entry.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") - obj.password = encrypt( - password_entry.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") - obj.notes = encrypt( - notes_entry.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") + obj = form.save(commit=False) obj.owner = request.user - - form.save() - messages.success( - request, - "Item created successfully.", - ) + obj.save() + messages.success(request, "Item created successfully.") return redirect("passmanager:vault") elif action == "generate_password": @@ -122,22 +108,10 @@ def edit_item(request, item_id): notes_entry = obj.notes if action == "save": - obj.username = encrypt( - username_entry.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") - obj.password = encrypt( - password_entry.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") - obj.notes = encrypt( - notes_entry.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") + obj = form.save(commit=False) obj.owner = request.user - - form.save() - messages.success( - request, - "Item modified successfully.", - ) + obj.save() + messages.success(request, "Item modified successfully.") return redirect("passmanager:vault") elif action == "generate_password": @@ -168,25 +142,8 @@ def edit_item(request, item_id): ) else: - # Decrypt the fields for display in the form - decrypted_username = decrypt( - item.username.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") - decrypted_password = decrypt( - item.password.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") - decrypted_notes = decrypt( - item.notes.encode(), os.getenv("ENCRYPTION_KEY") - ).decode("utf-8") - - initial_data = { - "name": item.name, - "username": decrypted_username, - "password": decrypted_password, - "url": item.url, - "notes": decrypted_notes, - } - form = ItemForm(instance=item, initial=initial_data) + item.decrypt_sensitive_fields() + form = ItemForm(instance=item) context = {"item": item, "form": form} return render(request, "passmanager/edit_item.html", context) diff --git a/users/migrations/0007_customuser_encryption_salt.py b/users/migrations/0007_customuser_encryption_salt.py new file mode 100644 index 0000000..92c87f4 --- /dev/null +++ b/users/migrations/0007_customuser_encryption_salt.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-01-08 17:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_alter_customuser_enable_2fa'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='encryption_salt', + field=models.CharField(blank=True, max_length=32, null=True), + ), + ] diff --git a/users/models.py b/users/models.py index e7c518c..afb2475 100644 --- a/users/models.py +++ b/users/models.py @@ -1,5 +1,7 @@ -from django.contrib.auth.models import AbstractUser +import os +import base64 from django.db import models +from django.contrib.auth.models import AbstractUser from django.utils.translation import gettext as _ from .managers import CustomUserManager @@ -15,6 +17,7 @@ class CustomUser(AbstractUser): email = models.EmailField(_("email address"), unique=True) + encryption_salt = models.CharField(max_length=32, blank=True, null=True) enable_2fa = models.BooleanField(default=False, verbose_name="Enable 2FA") otp_secret = models.CharField(max_length=32) session_timeout = models.IntegerField( @@ -27,5 +30,10 @@ class CustomUser(AbstractUser): objects = CustomUserManager() + def save(self, *args, **kwargs): + if not self.encryption_salt: + self.encryption_salt = base64.urlsafe_b64encode(os.urandom(16)).decode() + super().save(*args, **kwargs) + def __str__(self): return self.email