Skip to content

Commit

Permalink
Implement per-user encryption for sensitive data
Browse files Browse the repository at this point in the history
  • Loading branch information
KafetzisThomas committed Jan 8, 2025
1 parent f132cd4 commit 45b7742
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 53 deletions.
61 changes: 60 additions & 1 deletion passmanager/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
59 changes: 8 additions & 51 deletions passmanager/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions users/migrations/0007_customuser_encryption_salt.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
10 changes: 9 additions & 1 deletion users/models.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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(
Expand All @@ -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

0 comments on commit 45b7742

Please sign in to comment.