resolve merge conflicts

This commit is contained in:
Cayo Puigdefabregas 2024-01-17 14:11:47 +01:00
commit 646831a55e
15 changed files with 201 additions and 41 deletions

View file

@ -17,6 +17,7 @@ from django.views.generic.edit import (
UpdateView, UpdateView,
) )
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.core.cache import cache
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.http import HttpResponse from django.http import HttpResponse
from django.contrib import messages from django.contrib import messages
@ -645,13 +646,14 @@ class DidRegisterView(Credentials, CreateView):
def form_valid(self, form): def form_valid(self, form):
form.instance.user = self.request.user form.instance.user = self.request.user
form.instance.set_did() form.instance.set_did(cache.get("KEY_DIDS"))
form.save() form.save()
messages.success(self.request, _('DID created successfully')) messages.success(self.request, _('DID created successfully'))
Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance)
return super().form_valid(form) return super().form_valid(form)
class DidEditView(Credentials, UpdateView): class DidEditView(Credentials, UpdateView):
template_name = "idhub/admin/did_register.html" template_name = "idhub/admin/did_register.html"
subtitle = _('Organization Identities (DID)') subtitle = _('Organization Identities (DID)')

View file

@ -7,6 +7,7 @@ from utils import credtools
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache
from decouple import config from decouple import config
from idhub.models import DID, Schemas from idhub.models import DID, Schemas
from oidc4vp.models import Organization from oidc4vp.models import Organization
@ -36,17 +37,25 @@ class Command(BaseCommand):
self.create_organizations(r[0].strip(), r[1].strip()) self.create_organizations(r[0].strip(), r[1].strip())
self.sync_credentials_organizations("pangea.org", "somconnexio.coop") self.sync_credentials_organizations("pangea.org", "somconnexio.coop")
self.sync_credentials_organizations("local 8000", "local 9000") self.sync_credentials_organizations("local 8000", "local 9000")
self.create_defaults_dids()
self.create_schemas() self.create_schemas()
def create_admin_users(self, email, password): 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): 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_password(password)
u.set_encrypted_sensitive_data(password)
u.save() u.save()
key = u.decrypt_sensitive_data(password)
self.create_defaults_dids(u, key)
def create_organizations(self, name, url): def create_organizations(self, name, url):
@ -61,12 +70,10 @@ class Command(BaseCommand):
org1.my_client_secret = org2.client_secret org1.my_client_secret = org2.client_secret
org1.save() org1.save()
org2.save() org2.save()
def create_defaults_dids(self, u, password):
def create_defaults_dids(self): did = DID(label="Default", user=u, type=DID.Types.KEY)
for u in User.objects.all(): did.set_did(password)
did = DID(label="Default", user=u, type=DID.Types.KEY) did.save()
did.set_did()
did.save()
def create_schemas(self): def create_schemas(self):
schemas_files = os.listdir(settings.SCHEMAS_DIR) schemas_files = os.listdir(settings.SCHEMAS_DIR)

View file

@ -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.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -34,7 +34,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now=True)), ('created_at', models.DateTimeField(auto_now=True)),
('label', models.CharField(max_length=50, verbose_name='Label')), ('label', models.CharField(max_length=50, verbose_name='Label')),
('did', models.CharField(max_length=250)), ('did', models.CharField(max_length=250)),
('key_material', models.CharField(max_length=250)), ('key_material', models.CharField(max_length=255)),
('didweb_document', models.TextField()), ('didweb_document', models.TextField()),
( (
'user', 'user',

View file

@ -5,8 +5,11 @@ import datetime
from collections import OrderedDict from collections import OrderedDict
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from nacl import secret
from utils.idhub_ssikit import ( from utils.idhub_ssikit import (
generate_did_controller_key, generate_did_controller_key,
keydid_from_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. # In JWK format. Must be stored as-is and passed whole to library functions.
# Example key material: # Example key material:
# '{"kty":"OKP","crv":"Ed25519","x":"oB2cPGFx5FX4dtS1Rtep8ac6B__61HAP_RtSzJdPxqs","d":"OJw80T1CtcqV0hUcZdcI-vYNBN1dlubrLaJa0_se_gU"}' # '{"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 = models.ForeignKey(
User, User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -428,25 +431,32 @@ class DID(models.Model):
) )
didweb_document = models.TextField() 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 @property
def is_organization_did(self): def is_organization_did(self):
if not self.user: if not self.user:
return True return True
return False return False
def set_did(self): def set_did(self, password):
self.key_material = generate_did_controller_key() new_key_material = generate_did_controller_key()
self.set_key_material(new_key_material, password)
if self.type == self.Types.KEY: 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: 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.did = didurl
self.didweb_document = document self.didweb_document = document
def get_key(self): def get_key(self):
return json.loads(self.key_material) return json.loads(self.key_material)
class Schemas(models.Model): class Schemas(models.Model):
type = models.CharField(max_length=250) type = models.CharField(max_length=250)
file_schema = models.CharField(max_length=250) file_schema = models.CharField(max_length=250)
@ -518,6 +528,14 @@ class VerificableCredential(models.Model):
related_name='vcredentials', 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): def type(self):
return self.schema.type return self.schema.type
@ -547,20 +565,23 @@ class VerificableCredential(models.Model):
data = json.loads(self.csv_data).items() data = json.loads(self.csv_data).items()
return data return data
def issue(self, did): def issue(self, did, password):
if self.status == self.Status.ISSUED: if self.status == self.Status.ISSUED:
return return
# self.status = self.Status.ISSUED self.status = self.Status.ISSUED
self.subject_did = did self.subject_did = did
self.issued_on = datetime.datetime.now().astimezone(pytz.utc) self.issued_on = datetime.datetime.now().astimezone(pytz.utc)
d_ordered = ujson.loads(self.render()) issuer_pass = cache.get("KEY_DIDS")
d_minimum = self.filter_dict(d_ordered) # issuer_pass = self.user.decrypt_data(
data = ujson.dumps(d_minimum) # cache.get("KEY_DIDS"),
self.data = sign_credential( # settings.SECRET_KEY,
data, # )
self.issuer_did.key_material 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): def get_context(self):
d = json.loads(self.csv_data) d = json.loads(self.csv_data)
@ -596,7 +617,9 @@ class VerificableCredential(models.Model):
self.schema.file_schema self.schema.file_schema
) )
tmpl = get_template(template_name) 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): def get_issued_on(self):

View file

@ -17,7 +17,7 @@ Including another URLconf
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.views.generic import RedirectView from django.views.generic import RedirectView
from django.urls import path, reverse_lazy 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 .admin import views as views_admin
from .user import views as views_user from .user import views as views_user
# from .verification_portal import views as views_verification_portal # from .verification_portal import views as views_verification_portal
@ -45,13 +45,16 @@ urlpatterns = [
), ),
name='password_reset_done' name='password_reset_done'
), ),
path('auth/reset/<uidb64>/<token>/', path('auth/reset/<uidb64>/<token>/', PasswordResetConfirmView.as_view(),
auth_views.PasswordResetConfirmView.as_view(
template_name='auth/password_reset_confirm.html',
success_url=reverse_lazy('idhub:password_reset_complete')
),
name='password_reset_confirm' name='password_reset_confirm'
), ),
# path('auth/reset/<uidb64>/<token>/',
# 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/', path('auth/reset/done/',
auth_views.PasswordResetCompleteView.as_view( auth_views.PasswordResetCompleteView.as_view(
template_name='auth/password_reset_complete.html' template_name='auth/password_reset_complete.html'

View file

@ -24,6 +24,7 @@ class RequestCredentialForm(forms.Form):
self.user = kwargs.pop('user', None) self.user = kwargs.pop('user', None)
self.lang = kwargs.pop('lang', None) self.lang = kwargs.pop('lang', None)
self._domain = kwargs.pop('domain', None) self._domain = kwargs.pop('domain', None)
self.password = kwargs.pop('password', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['did'].choices = [ self.fields['did'].choices = [
(x.did, x.label) for x in DID.objects.filter(user=self.user) (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 = cred[0]
cred._domain = self._domain cred._domain = self._domain
try: try:
cred.issue(did) if self.password:
cred.issue(did, self.password)
except Exception: except Exception:
return return

View file

@ -120,7 +120,15 @@ class CredentialJsonView(MyWallet, TemplateView):
pk=pk, pk=pk,
user=self.request.user 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") response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json")
return response return response
@ -138,6 +146,15 @@ class CredentialsRequestView(MyWallet, FormView):
kwargs['lang'] = self.request.LANGUAGE_CODE kwargs['lang'] = self.request.LANGUAGE_CODE
domain = "{}://{}".format(self.request.scheme, self.request.get_host()) domain = "{}://{}".format(self.request.scheme, self.request.get_host())
kwargs['domain'] = domain 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 return kwargs
def form_valid(self, form): def form_valid(self, form):
@ -208,7 +225,11 @@ class DidRegisterView(MyWallet, CreateView):
def form_valid(self, form): def form_valid(self, form):
form.instance.user = self.request.user 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() form.save()
messages.success(self.request, _('DID created successfully')) messages.success(self.request, _('DID created successfully'))

View file

@ -1,5 +1,7 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy 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.utils.translation import gettext_lazy as _
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.contrib.auth import login as auth_login from django.contrib.auth import login as auth_login
@ -25,13 +27,41 @@ class LoginView(auth_views.LoginView):
def form_valid(self, form): def form_valid(self, form):
user = form.get_user() 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: if not user.is_anonymous and user.is_admin:
admin_dashboard = reverse_lazy('idhub:admin_dashboard') admin_dashboard = reverse_lazy('idhub:admin_dashboard')
self.extra_context['success_url'] = 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']) 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): def serve_did(request, did_id):
id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}' id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}'
did = get_object_or_404(DID, did=id_did) did = get_object_or_404(DID, did=id_did)

View file

@ -31,4 +31,3 @@ class ProfileForm(forms.ModelForm):
return last_name return last_name

View file

@ -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 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' 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={ options={
'abstract': False, 'abstract': False,

View file

@ -1,4 +1,9 @@
import nacl
import base64
from nacl import pwhash
from django.db import models from django.db import models
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
@ -44,6 +49,8 @@ class User(AbstractBaseUser):
is_admin = models.BooleanField(default=False) is_admin = models.BooleanField(default=False)
first_name = models.CharField(_("First name"), max_length=255, blank=True, null=True) 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) 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() objects = UserManager()
@ -86,3 +93,65 @@ class User(AbstractBaseUser):
for r in s.service.rol.all(): for r in s.service.rol.all():
roles.append(r.name) roles.append(r.name)
return ", ".join(set(roles)) 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)

View file

@ -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.conf import settings
from django.db import migrations, models from django.db import migrations, models

View file

@ -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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion

View file

@ -12,6 +12,7 @@ requests==2.31.0
jinja2==3.1.2 jinja2==3.1.2
jsonref==1.1.0 jsonref==1.1.0
pyld==2.0.3 pyld==2.0.3
pynacl==1.5.0
more-itertools==10.1.0 more-itertools==10.1.0
dj-database-url==2.1.0 dj-database-url==2.1.0
ujson==5.9.0 ujson==5.9.0

View file

@ -149,6 +149,7 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://docs.djangoproject.com/en/4.2/topics/i18n/