diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 5d8d15c..44e36af 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -17,6 +17,7 @@ from django.views.generic.edit import ( UpdateView, ) from django.shortcuts import get_object_or_404, redirect +from django.core.cache import cache from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages @@ -645,13 +646,14 @@ class DidRegisterView(Credentials, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + form.instance.set_did(cache.get("KEY_DIDS")) form.save() messages.success(self.request, _('DID created successfully')) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) return super().form_valid(form) + class DidEditView(Credentials, UpdateView): template_name = "idhub/admin/did_register.html" subtitle = _('Organization Identities (DID)') diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index e7082f7..2dd6ad6 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -7,6 +7,7 @@ from utils import credtools from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model +from django.core.cache import cache from decouple import config from idhub.models import DID, Schemas from oidc4vp.models import Organization @@ -36,17 +37,25 @@ class Command(BaseCommand): self.create_organizations(r[0].strip(), r[1].strip()) self.sync_credentials_organizations("pangea.org", "somconnexio.coop") self.sync_credentials_organizations("local 8000", "local 9000") - self.create_defaults_dids() self.create_schemas() def create_admin_users(self, email, password): - User.objects.create_superuser(email=email, password=password) + su = User.objects.create_superuser(email=email, password=password) + su.set_encrypted_sensitive_data(password) + su.save() + key = su.decrypt_sensitive_data(password) + key_dids = {su.id: key} + cache.set("KEY_DIDS", key_dids, None) + self.create_defaults_dids(su, key) def create_users(self, email, password): - u= User.objects.create(email=email, password=password) + u = User.objects.create(email=email, password=password) u.set_password(password) + u.set_encrypted_sensitive_data(password) u.save() + key = u.decrypt_sensitive_data(password) + self.create_defaults_dids(u, key) def create_organizations(self, name, url): @@ -61,12 +70,10 @@ class Command(BaseCommand): org1.my_client_secret = org2.client_secret org1.save() org2.save() - - def create_defaults_dids(self): - for u in User.objects.all(): - did = DID(label="Default", user=u, type=DID.Types.KEY) - did.set_did() - did.save() + def create_defaults_dids(self, u, password): + did = DID(label="Default", user=u, type=DID.Types.KEY) + did.set_did(password) + did.save() def create_schemas(self): schemas_files = os.listdir(settings.SCHEMAS_DIR) diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 72ae46f..321e70a 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-16 13:04 +# Generated by Django 4.2.5 on 2024-01-17 13:11 from django.conf import settings from django.db import migrations, models @@ -34,7 +34,7 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now=True)), ('label', models.CharField(max_length=50, verbose_name='Label')), ('did', models.CharField(max_length=250)), - ('key_material', models.CharField(max_length=250)), + ('key_material', models.CharField(max_length=255)), ('didweb_document', models.TextField()), ( 'user', diff --git a/idhub/models.py b/idhub/models.py index c4da900..d2199c3 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -5,8 +5,11 @@ import datetime from collections import OrderedDict from django.db import models from django.conf import settings +from django.core.cache import cache from django.template.loader import get_template from django.utils.translation import gettext_lazy as _ +from nacl import secret + from utils.idhub_ssikit import ( generate_did_controller_key, keydid_from_controller_key, @@ -419,7 +422,7 @@ class DID(models.Model): # In JWK format. Must be stored as-is and passed whole to library functions. # Example key material: # '{"kty":"OKP","crv":"Ed25519","x":"oB2cPGFx5FX4dtS1Rtep8ac6B__61HAP_RtSzJdPxqs","d":"OJw80T1CtcqV0hUcZdcI-vYNBN1dlubrLaJa0_se_gU"}' - key_material = models.CharField(max_length=250) + key_material = models.CharField(max_length=255) user = models.ForeignKey( User, on_delete=models.CASCADE, @@ -428,25 +431,32 @@ class DID(models.Model): ) didweb_document = models.TextField() + def get_key_material(self, password): + return self.user.decrypt_data(self.key_material, password) + + def set_key_material(self, value, password): + self.key_material = self.user.encrypt_data(value, password) + @property def is_organization_did(self): if not self.user: return True return False - def set_did(self): - self.key_material = generate_did_controller_key() + def set_did(self, password): + new_key_material = generate_did_controller_key() + self.set_key_material(new_key_material, password) + if self.type == self.Types.KEY: - self.did = keydid_from_controller_key(self.key_material) + self.did = keydid_from_controller_key(new_key_material) elif self.type == self.Types.WEB: - didurl, document = webdid_from_controller_key(self.key_material) + didurl, document = webdid_from_controller_key(new_key_material) self.did = didurl self.didweb_document = document def get_key(self): return json.loads(self.key_material) - class Schemas(models.Model): type = models.CharField(max_length=250) file_schema = models.CharField(max_length=250) @@ -518,6 +528,14 @@ class VerificableCredential(models.Model): related_name='vcredentials', ) + def get_data(self, password): + if not self.data: + return "" + return self.user.decrypt_data(self.data, password) + + def set_data(self, value, password): + self.data = self.user.encrypt_data(value, password) + def type(self): return self.schema.type @@ -547,20 +565,23 @@ class VerificableCredential(models.Model): data = json.loads(self.csv_data).items() return data - def issue(self, did): + def issue(self, did, password): if self.status == self.Status.ISSUED: return - # self.status = self.Status.ISSUED + self.status = self.Status.ISSUED self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) - d_ordered = ujson.loads(self.render()) - d_minimum = self.filter_dict(d_ordered) - data = ujson.dumps(d_minimum) - self.data = sign_credential( - data, - self.issuer_did.key_material + issuer_pass = cache.get("KEY_DIDS") + # issuer_pass = self.user.decrypt_data( + # cache.get("KEY_DIDS"), + # settings.SECRET_KEY, + # ) + data = sign_credential( + self.render(), + self.issuer_did.get_key_material(issuer_pass) ) + self.data = self.user.encrypt_data(data, password) def get_context(self): d = json.loads(self.csv_data) @@ -596,7 +617,9 @@ class VerificableCredential(models.Model): self.schema.file_schema ) tmpl = get_template(template_name) - return tmpl.render(context) + d_ordered = ujson.loads(tmpl.render(context)) + d_minimum = self.filter_dict(d_ordered) + return ujson.dumps(d_minimum) def get_issued_on(self): diff --git a/idhub/urls.py b/idhub/urls.py index 8b92d93..5d52840 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -17,7 +17,7 @@ Including another URLconf from django.contrib.auth import views as auth_views from django.views.generic import RedirectView from django.urls import path, reverse_lazy -from .views import LoginView, serve_did +from .views import LoginView, PasswordResetConfirmView, serve_did from .admin import views as views_admin from .user import views as views_user # from .verification_portal import views as views_verification_portal @@ -45,13 +45,16 @@ urlpatterns = [ ), name='password_reset_done' ), - path('auth/reset///', - auth_views.PasswordResetConfirmView.as_view( - template_name='auth/password_reset_confirm.html', - success_url=reverse_lazy('idhub:password_reset_complete') - ), + path('auth/reset///', PasswordResetConfirmView.as_view(), name='password_reset_confirm' ), + # path('auth/reset///', + # auth_views.PasswordResetConfirmView.as_view( + # template_name='auth/password_reset_confirm.html', + # success_url=reverse_lazy('idhub:password_reset_complete') + # ), + # name='password_reset_confirm' + # ), path('auth/reset/done/', auth_views.PasswordResetCompleteView.as_view( template_name='auth/password_reset_complete.html' diff --git a/idhub/user/forms.py b/idhub/user/forms.py index f9eda6a..e5530fa 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -24,6 +24,7 @@ class RequestCredentialForm(forms.Form): self.user = kwargs.pop('user', None) self.lang = kwargs.pop('lang', None) self._domain = kwargs.pop('domain', None) + self.password = kwargs.pop('password', None) super().__init__(*args, **kwargs) self.fields['did'].choices = [ (x.did, x.label) for x in DID.objects.filter(user=self.user) @@ -52,7 +53,8 @@ class RequestCredentialForm(forms.Form): cred = cred[0] cred._domain = self._domain try: - cred.issue(did) + if self.password: + cred.issue(did, self.password) except Exception: return diff --git a/idhub/user/views.py b/idhub/user/views.py index 29153e6..37144cb 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -120,7 +120,15 @@ class CredentialJsonView(MyWallet, TemplateView): pk=pk, user=self.request.user ) - response = HttpResponse(self.object.data, content_type="application/json") + pass_enc = self.request.session.get("key_did") + data = "" + if pass_enc: + user_pass = self.request.user.decrypt_data( + pass_enc, + self.request.user.password+self.request.session._session_key + ) + data = self.object.get_data(user_pass) + response = HttpResponse(data, content_type="application/json") response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json") return response @@ -138,6 +146,15 @@ class CredentialsRequestView(MyWallet, FormView): kwargs['lang'] = self.request.LANGUAGE_CODE domain = "{}://{}".format(self.request.scheme, self.request.get_host()) kwargs['domain'] = domain + pass_enc = self.request.session.get("key_did") + if pass_enc: + user_pass = self.request.user.decrypt_data( + pass_enc, + self.request.user.password+self.request.session._session_key + ) + else: + pass_enc = None + kwargs['password'] = user_pass return kwargs def form_valid(self, form): @@ -208,7 +225,11 @@ class DidRegisterView(MyWallet, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + pw = self.request.user.decrypt_data( + self.request.session.get("key_did"), + self.request.user.password+self.request.session._session_key + ) + form.instance.set_did(pw) form.save() messages.success(self.request, _('DID created successfully')) diff --git a/idhub/views.py b/idhub/views.py index 851011f..f513353 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,5 +1,7 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy +from django.conf import settings +from django.core.cache import cache from django.utils.translation import gettext_lazy as _ from django.contrib.auth import views as auth_views from django.contrib.auth import login as auth_login @@ -25,13 +27,41 @@ class LoginView(auth_views.LoginView): def form_valid(self, form): user = form.get_user() + password = form.cleaned_data.get("password") + auth_login(self.request, user) + + sensitive_data_encryption_key = user.decrypt_sensitive_data(password) + if not user.is_anonymous and user.is_admin: admin_dashboard = reverse_lazy('idhub:admin_dashboard') self.extra_context['success_url'] = admin_dashboard - auth_login(self.request, user) + # encryption_key = user.encrypt_data( + # sensitive_data_encryption_key, + # settings.SECRET_KEY + # ) + # cache.set("KEY_DIDS", encryption_key, None) + cache.set("KEY_DIDS", sensitive_data_encryption_key, None) + + self.request.session["key_did"] = user.encrypt_data( + sensitive_data_encryption_key, + user.password+self.request.session._session_key + ) + return HttpResponseRedirect(self.extra_context['success_url']) +class PasswordResetConfirmView(auth_views.PasswordResetConfirmView): + template_name = 'auth/password_reset_confirm.html' + success_url = reverse_lazy('idhub:password_reset_complete') + + def form_valid(self, form): + password = form.cleaned_data.get("password") + user = form.get_user() + user.set_encrypted_sensitive_data(password) + user.save() + return HttpResponseRedirect(self.success_url) + + def serve_did(request, did_id): id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}' did = get_object_or_404(DID, did=id_did) diff --git a/idhub_auth/forms.py b/idhub_auth/forms.py index f4279b7..d9ff2f7 100644 --- a/idhub_auth/forms.py +++ b/idhub_auth/forms.py @@ -31,4 +31,3 @@ class ProfileForm(forms.ModelForm): return last_name - diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index dd15670..ee8f4a9 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-16 13:04 +# Generated by Django 4.2.5 on 2024-01-17 13:11 from django.db import migrations, models @@ -48,6 +48,8 @@ class Migration(migrations.Migration): blank=True, max_length=255, null=True, verbose_name='Last name' ), ), + ('encrypted_sensitive_data', models.CharField(max_length=255)), + ('salt', models.CharField(max_length=255)), ], options={ 'abstract': False, diff --git a/idhub_auth/models.py b/idhub_auth/models.py index 07a7896..38224b2 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -1,4 +1,9 @@ +import nacl +import base64 + +from nacl import pwhash from django.db import models +from django.core.cache import cache from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import BaseUserManager, AbstractBaseUser @@ -44,6 +49,8 @@ class User(AbstractBaseUser): is_admin = models.BooleanField(default=False) first_name = models.CharField(_("First name"), max_length=255, blank=True, null=True) last_name = models.CharField(_("Last name"), max_length=255, blank=True, null=True) + encrypted_sensitive_data = models.CharField(max_length=255) + salt = models.CharField(max_length=255) objects = UserManager() @@ -86,3 +93,65 @@ class User(AbstractBaseUser): for r in s.service.rol.all(): roles.append(r.name) return ", ".join(set(roles)) + + def derive_key_from_password(self, password): + kdf = pwhash.argon2i.kdf + ops = pwhash.argon2i.OPSLIMIT_INTERACTIVE + mem = pwhash.argon2i.MEMLIMIT_INTERACTIVE + return kdf( + nacl.secret.SecretBox.KEY_SIZE, + password, + self.get_salt(), + opslimit=ops, + memlimit=mem + ) + + def decrypt_sensitive_data(self, password, data=None): + sb_key = self.derive_key_from_password(password.encode('utf-8')) + sb = nacl.secret.SecretBox(sb_key) + if not data: + data = self.get_encrypted_sensitive_data() + if not isinstance(data, bytes): + data = data.encode('utf-8') + + return sb.decrypt(data).decode('utf-8') + + def encrypt_sensitive_data(self, password, data): + sb_key = self.derive_key_from_password(password.encode('utf-8')) + sb = nacl.secret.SecretBox(sb_key) + if not isinstance(data, bytes): + data = data.encode('utf-8') + + return base64.b64encode(sb.encrypt(data)).decode('utf-8') + + def get_salt(self): + return base64.b64decode(self.salt.encode('utf-8')) + + def set_salt(self): + self.salt = base64.b64encode(nacl.utils.random(16)).decode('utf-8') + + def get_encrypted_sensitive_data(self): + return base64.b64decode(self.encrypted_sensitive_data.encode('utf-8')) + + def set_encrypted_sensitive_data(self, password): + key = base64.b64encode(nacl.utils.random(64)) + self.set_salt() + + key_crypted = self.encrypt_sensitive_data(password, key) + self.encrypted_sensitive_data = key_crypted + + def encrypt_data(self, data, password): + sb = self.get_secret_box(password) + value_enc = sb.encrypt(data.encode('utf-8')) + return base64.b64encode(value_enc).decode('utf-8') + + def decrypt_data(self, data, password): + # import pdb; pdb.set_trace() + sb = self.get_secret_box(password) + value = base64.b64decode(data.encode('utf-8')) + return sb.decrypt(value).decode('utf-8') + + def get_secret_box(self, password): + pw = base64.b64decode(password.encode('utf-8')*4) + sb_key = self.derive_key_from_password(pw) + return nacl.secret.SecretBox(sb_key) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 2b7618b..586b4e1 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-16 13:04 +# Generated by Django 4.2.5 on 2024-01-17 13:11 from django.conf import settings from django.db import migrations, models diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py index 85bdefd..1aff341 100644 --- a/promotion/migrations/0001_initial.py +++ b/promotion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-16 13:04 +# Generated by Django 4.2.5 on 2024-01-17 13:11 from django.db import migrations, models import django.db.models.deletion diff --git a/requirements.txt b/requirements.txt index 13da29d..0c859a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ requests==2.31.0 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 +pynacl==1.5.0 more-itertools==10.1.0 dj-database-url==2.1.0 ujson==5.9.0 diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 6091fe0..7c450b7 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -149,6 +149,7 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/