From 37dc8335a72d48e1f550f0f11f524875d61904c6 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Fri, 1 Dec 2023 06:39:26 +0100 Subject: [PATCH 01/70] Changed model definitions and added logic to decrypt and set user key in session storage --- idhub/models.py | 38 +++++++++++++++++++++++++++++++++++--- idhub/views.py | 4 ++++ idhub_auth/models.py | 18 ++++++++++++++++++ requirements.txt | 1 + 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 53f8186..fbf00ef 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -6,6 +6,8 @@ from django.db import models from django.conf import settings 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, @@ -409,7 +411,9 @@ 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) + # CHANGED: `key_material` to `_key_material`, datatype from CharField to BinaryField and the key is now stored encrypted. + key_material = None + _key_material = models.BinaryField(max_length=250) user = models.ForeignKey( User, on_delete=models.CASCADE, @@ -417,6 +421,18 @@ class DID(models.Model): null=True, ) + def get_key_material(self, session): + if "sensitive_data_encryption_key" not in session: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") + sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + return sb.decrypt(self._key_material) + + def set_key_material(self, value, session): + if "sensitive_data_encryption_key" not in session: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") + sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + self._key_material = sb.encrypt(value) + @property def is_organization_did(self): if not self.user: @@ -427,7 +443,8 @@ class DID(models.Model): self.key_material = generate_did_controller_key() self.did = keydid_from_controller_key(self.key_material) - def get_key(self): + # TODO: darmengo: esta funcion solo se llama desde un fichero que sube cosas a s3 (??) Preguntar a ver que hace. + def get_key_deprecated(self): return json.loads(self.key_material) @@ -464,7 +481,10 @@ class VerificableCredential(models.Model): created_on = models.DateTimeField(auto_now=True) issued_on = models.DateTimeField(null=True) subject_did = models.CharField(max_length=250) - data = models.TextField() + # CHANGED: `data` to `_data`, datatype from TextField to BinaryField and the rendered VC is now stored encrypted. + # TODO: verify that BinaryField can hold arbitrary amounts of data (max_length = ???) + data = None + _data = models.BinaryField() csv_data = models.TextField() status = models.PositiveSmallIntegerField( choices=Status.choices, @@ -486,6 +506,18 @@ class VerificableCredential(models.Model): related_name='vcredentials', ) + def get_data(self, session): + if "sensitive_data_encryption_key" not in session: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") + sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + return sb.decrypt(self._data) + + def set_data(self, value, session): + if "sensitive_data_encryption_key" not in session: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") + sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + self._data = sb.encrypt(value) + @property def get_schema(self): if not self.data: diff --git a/idhub/views.py b/idhub/views.py index 53db736..f8a62a7 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -25,4 +25,8 @@ class LoginView(auth_views.LoginView): if self.extra_context['success_url'] == user_dashboard: self.extra_context['success_url'] = admin_dashboard auth_login(self.request, user) + # Decrypt the user's sensitive data encryption key and store it in the session. + password = form.cleaned_data.get("password") # TODO: Is this right???????? + sensitive_data_encryption_key = user.decrypt_sensitive_data_encryption_key(password) + self.request.session["sensitive_data_encryption_key"] = sensitive_data_encryption_key return HttpResponseRedirect(self.extra_context['success_url']) diff --git a/idhub_auth/models.py b/idhub_auth/models.py index ccda94c..cf1ac69 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -1,5 +1,6 @@ from django.db import models from django.contrib.auth.models import BaseUserManager, AbstractBaseUser +from nacl import secret, pwhash class UserManager(BaseUserManager): @@ -43,6 +44,10 @@ class User(AbstractBaseUser): is_admin = models.BooleanField(default=False) first_name = models.CharField(max_length=255, blank=True, null=True) last_name = models.CharField(max_length=255, blank=True, null=True) + # TODO: Hay que generar una clave aleatoria para cada usuario cuando se le da de alta en el sistema. + encrypted_sensitive_data_encryption_key = models.BinaryField(max_length=255) + # TODO: Hay que generar un salt aleatorio para cada usuario cuando se le da de alta en el sistema. + salt_of_sensitive_data_encryption_key = models.BinaryField(max_length=255) objects = UserManager() @@ -85,3 +90,16 @@ 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 # TODO: Move the KDF choice to SETTINGS.PY + ops = pwhash.argon2i.OPSLIMIT_INTERACTIVE # TODO: Move the KDF choice to SETTINGS.PY + mem = pwhash.argon2i.MEMLIMIT_INTERACTIVE # TODO: Move the KDF choice to SETTINGS.PY + salt = self.salt_of_sensitive_data_encryption_key + return kdf(secret.SecretBox.KEY_SIZE, password, salt, opslimit=ops, memlimit=mem) + + def decrypt_sensitive_data_encryption_key(self, password): + sb_key = self.derive_key_from_password(password) + sb = secret.SecretBox(sb_key) + return sb.decrypt(self.encrypted_sensitive_data_encryption_key) + diff --git a/requirements.txt b/requirements.txt index 140c49f..c09c9d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 +pynacl==1.5.0 From 20f40b43d049d325ccc33b97bef0da784ac7d380 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Fri, 1 Dec 2023 07:01:51 +0100 Subject: [PATCH 02/70] Refactored all uses of DID.key_material --- idhub/admin/views.py | 2 +- idhub/models.py | 17 ++++++++++++----- idhub/user/forms.py | 3 ++- idhub/user/views.py | 3 ++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index f8fd6d0..1725339 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -645,7 +645,7 @@ class DidRegisterView(Credentials, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + form.instance.set_did(self.request.session) form.save() messages.success(self.request, _('DID created successfully')) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) diff --git a/idhub/models.py b/idhub/models.py index fbf00ef..6a326b9 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -439,9 +439,16 @@ class DID(models.Model): return True return False - def set_did(self): - self.key_material = generate_did_controller_key() - self.did = keydid_from_controller_key(self.key_material) + def set_did(self, session): + """ + Generates a new DID Controller Key and derives a DID from it. + Because DID Controller Keys are stored encrypted using a User's Sensitive Data Encryption Key, + this function needs to be called in the context of a request. + """ + new_key_material = generate_did_controller_key() + self.did = keydid_from_controller_key(new_key_material) + self.set_key_material(new_key_material, session) + # TODO: darmengo: esta funcion solo se llama desde un fichero que sube cosas a s3 (??) Preguntar a ver que hace. def get_key_deprecated(self): @@ -546,7 +553,7 @@ class VerificableCredential(models.Model): data = json.loads(self.csv_data).items() return data - def issue(self, did): + def issue(self, did, session): if self.status == self.Status.ISSUED: return @@ -555,7 +562,7 @@ class VerificableCredential(models.Model): self.issued_on = datetime.datetime.now().astimezone(pytz.utc) self.data = sign_credential( self.render(), - self.issuer_did.key_material + self.issuer_did.get_key_material(session) ) def get_context(self): diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 53a1149..3735d64 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -18,6 +18,7 @@ class RequestCredentialForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) + self.session = kwargs.pop('session', None) super().__init__(*args, **kwargs) self.fields['did'].choices = [ (x.did, x.label) for x in DID.objects.filter(user=self.user) @@ -45,7 +46,7 @@ class RequestCredentialForm(forms.Form): did = did[0].did cred = cred[0] try: - cred.issue(did) + cred.issue(did, self.session) except Exception: return diff --git a/idhub/user/views.py b/idhub/user/views.py index 482b40e..d59f7d6 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -128,6 +128,7 @@ class CredentialsRequestView(MyWallet, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + kwargs['session'] = self.request.session return kwargs def form_valid(self, form): @@ -189,7 +190,7 @@ class DidRegisterView(MyWallet, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + form.instance.set_did(self.request.session) form.save() messages.success(self.request, _('DID created successfully')) From 45f30f36649ddb298f8dc03f771e8891223c8029 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 15 Dec 2023 17:52:08 +0100 Subject: [PATCH 03/70] new credential membership --- examples/membership-card.csv | 4 +- .../credentials/membership-card.json | 162 +++++++----------- schemas/membership-card.json | 151 +++++++++------- 3 files changed, 154 insertions(+), 163 deletions(-) diff --git a/examples/membership-card.csv b/examples/membership-card.csv index 1f4d08d..7447dcc 100644 --- a/examples/membership-card.csv +++ b/examples/membership-card.csv @@ -1,2 +1,2 @@ -name surnames email typeOfPerson membershipType organisation affiliatedSince -Pepe Gómez user1@example.org individual Member Pangea 01-01-2023 +firstName lastName email membershipType membershipId affiliatedUntil affiliatedSince typeOfPerson identityDocType identityNumber +Pepe Gómez user1@example.org individual 123456 2024-01-01T00:00:00Z 2023-01-01T00:00:00Z natural DNI 12345678A diff --git a/idhub/templates/credentials/membership-card.json b/idhub/templates/credentials/membership-card.json index bc315bb..e618833 100644 --- a/idhub/templates/credentials/membership-card.json +++ b/idhub/templates/credentials/membership-card.json @@ -1,105 +1,67 @@ { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - { - "individual": "https://schema.org/Person", - "Member": "https://schema.org/Member", - "startDate": "https://schema.org/startDate", - "jsonSchema": "https://schema.org/jsonSchema", - "$ref": "https://schema.org/jsonSchemaRef", - "credentialSchema": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#credentialSchema", - "organisation": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#organisation", - "membershipType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipType", - "membershipId": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipId", - "typeOfPerson": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#typeOfPerson", - "identityDocType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityDocType", - "identityNumber": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityNumber", - "name": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#name", - "description": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#description", - "value": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#value", - "lang": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#lang", - "surnames": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#surnames", - "email": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#email", - "affiliatedSince": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedSince", - "affiliatedUntil": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedUntil", - "issued": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#issued", - "validFrom": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validFrom", - "validUntil": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validUntil" - } - ], - "type": [ - "VerifiableCredential", - "VerifiableAttestation", - "MembershipCard" - ], - "id": "{{ vc_id }}", - "issuer": { - "id": "{{ issuer_did }}", - "name": "Pangea", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://idhub.pangea.org/credentials/base/v1", + "https://idhub.pangea.org/credentials/membership-card/v1" + ], + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "MembershipCard" + ], + "id": "https://idhub.pangea.org/credentials/987654321", + "issuer": { + "id": "did:example:5678", + "name": "Pangea Internet Solidari" + }, + "issuanceDate": "2023-12-06T19:23:24Z", + "issued": "2023-12-06T19:23:24Z", + "validFrom": "2023-12-06T19:23:24Z", + "validUntil": "2024-12-06T19:23:24Z", + "name": [ + { + "value": "Membership Card", + "lang": "en" + }, + { + "value": "Carnet de soci/a", + "lang": "ca_ES" + }, + { + "value": "Carnet de socio/a", + "lang": "es" + } + ], "description": [ - { - "value": "Pangea.org is a service provider leveraging open-source technologies to provide affordable and accessible solutions for social enterprises and solidarity organisations.", - "lang": "en" - }, - { - "value": "Pangea.org és un proveïdor de serveis que aprofita les tecnologies de codi obert per oferir solucions assequibles i accessibles per a empreses socials i organitzacions solidàries.", - "lang": "ca_ES" - }, - { - "value": "Pangea.org es un proveedor de servicios que aprovecha tecnologías de código abierto para proporcionar soluciones asequibles y accesibles para empresas sociales y organizaciones solidarias.", - "lang": "es" - } - - ] - }, - "issuanceDate": "{{ issuance_date }}", - "issued": "{{ issuance_date }}", - "validFrom": "{{ issuance_date }}", - "validUntil": "{{ validUntil }}", - "name": [ - { - "value": "Membership Card", - "lang": "en" + { + "value": "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", + "lang": "en" + }, + { + "value": "El carnet de soci especifica la subscripció o la inscripció d'un individu en serveis o beneficis específics emesos per una organització.", + "lang": "ca_ES" + }, + { + "value": "El carnet de socio especifica la suscripción o inscripción de un individuo en servicios o beneficios específicos emitidos por uns organización.", + "lang": "es" + } + ], + "credentialSubject": { + "id": "did:example:1234", + "firstName": "Joan", + "lastName": "Pera", + "email": "joan.pera@pangea.org", + "typeOfPerson": "natural", + "identityDocType": "DNI", + "identityNumber": "12345678A", + "organisation": "Pangea", + "membershipType": "individual", + "membershipId": "123456", + "affiliatedSince": "2023-01-01T00:00:00Z", + "affiliatedUntil": "2024-01-01T00:00:00Z" }, - { - "value": "Carnet de soci/a", - "lang": "ca_ES" - }, - { - "value": "Carnet de socio/a", - "lang": "es" - } - ], - "description": [ - { - "value": "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", - "lang": "en" - }, - { - "value": "El carnet de soci especifica la subscripció o la inscripció d'un individu en serveis o beneficis específics emesos per una organització.", - "lang": "ca_ES" - }, - { - "value": "El carnet de socio especifica la suscripción o inscripción de un individuo en servicios o beneficios específicos emitidos por uns organización.", - "lang": "es" - } - ], - "credentialSubject": { - "id": "{{ subject_did }}", - "organisation": "Pangea", - "membershipType": "{{ membershipType }}", - "membershipId": "{{ vc_id }}", - "affiliatedSince": "{{ affiliatedSince }}", - "affiliatedUntil": "{{ affiliatedUntil }}", - "typeOfPerson": "{{ typeOfPerson }}", - "identityDocType": "{{ identityDocType }}", - "identityNumber": "{{ identityNumber }}", - "name": "{{ first_name }}", - "surnames": "{{ last_name }}", - "email": "{{ email }}", "credentialSchema": { - "id": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/membership-card-schema.json", - "type": "JsonSchema" + "id": "https://idhub.pangea.org/vc_schemas/membership-card.json", + "type": "FullJsonSchemaValidator2021" } - } -} \ No newline at end of file +} diff --git a/schemas/membership-card.json b/schemas/membership-card.json index 0d5ff9a..20a5f9b 100644 --- a/schemas/membership-card.json +++ b/schemas/membership-card.json @@ -1,65 +1,94 @@ { - "$id": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/membership-card-schema.json", + "$id": "https://idhub.pangea.org/vc_schemas/membership-card.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "name": "MembershipCard", - "description": "MembershipCard credential using JsonSchema", - "type": "object", - "properties": { - "credentialSubject": { - "type": "object", - "properties": { - "organisation": { - "type": "string" - }, - "membershipType": { - "type": "string" - }, - "affiliatedSince": { - "type": "string", - "format": "date-time" - }, - "affiliatedUntil": { - "type": "string", - "format": "date-time" - }, - "typeOfPerson": { - "type": "string", - "enum": [ - "individual", - "org" - ] - }, - "identityDocType": { - "type": "string", - "enum": [ - "DNI", - "NIF", - "NIE", - "PASSPORT" - ] - }, - "identityNumber": { - "type": "string" - }, - "name": { - "type": "string" - }, - "surnames": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - } - }, - "required": [ - "organisation", - "affiliatedSince", - "typeOfPerson", - "name", - "surnames", - "email" - ] + "title": "Membership Card", + "description": "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", + "name": [ + { + "value": "Membership Card", + "lang": "en" + }, + { + "value": "Carnet de soci/a", + "lang": "ca_ES" + }, + { + "value": "Carnet de socio/a", + "lang": "es" } - } + ], + "type": "object", + "allOf": [ + { + "$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json" + }, + { + "properties": { + "credentialSubject": { + "description": "Defines additional properties on credentialSubject", + "type": "object", + "properties": { + "id": { + "description": "Defines a unique identifier of the credential subject", + "type": "string" + }, + "organisation": { + "description": "Organisation the credential subject is affiliated with", + "type": "string" + }, + "membershipType": { + "description": "Type of membership", + "type": "string" + }, + "membershipId": { + "description": "Membership identifier", + "type": "string" + }, + "affiliatedSince": { + "type": "string", + "format": "date-time" + }, + "affiliatedUntil": { + "type": "string", + "format": "date-time" + }, + "typeOfPerson": { + "type": "string", + "enum": [ + "natural", + "legal" + ] + }, + "identityDocType": { + "description": "Type of the Identity Document of the credential subject", + "type": "string" + }, + "identityNumber": { + "description": "Number of the Identity Document of the credential subject", + "type": "string" + }, + "firstName": { + "description": "Name of the natural person or name of the legal person (organisation)", + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "id", + "organisation", + "affiliatedSince", + "typeOfPerson", + "firstName", + "email" + ] + } + } + } + ] } \ No newline at end of file From e551d2aadcc4ad1ba5245ff8c06f1c933e0ab192 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 15 Dec 2023 17:52:23 +0100 Subject: [PATCH 04/70] fix name --- idhub/models.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 7d1ef6c..8e3785e 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -443,9 +443,19 @@ class Schemas(models.Model): return {} return json.loads(self.data) - def name(self): - return self.get_schema.get('name', '') + def name(self, request=None): + names = {} + for name in self.get_schema.get('name', []): + lang = name.get('lang') + if 'ca' in lang: + lang = 'ca' + names[lang]= name.get('value') + if request and request.LANGUAGE_CODE in names.keys(): + return names[request.LANGUAGE_CODE] + + return names[settings.LANGUAGE_CODE] + def description(self): return self.get_schema.get('description', '') From 5d6580f73fa3af8274a23a7993dc5e86f5a55a9e Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 18 Dec 2023 12:00:00 +0100 Subject: [PATCH 05/70] fix validate1 --- idhub/admin/forms.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 2649f4b..69e8f3a 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -62,7 +62,8 @@ class ImportForm(forms.Form): self._schema = schema.first() try: self.json_schema = json.loads(self._schema.data) - prop = self.json_schema['properties'] + props = [x for x in self.json_schema["allOf"] if 'properties' in x] + prop = props[0]['properties'] self.properties = prop['credentialSubject']['properties'] except Exception: raise ValidationError("Schema is not valid!") @@ -110,8 +111,10 @@ class ImportForm(forms.Form): return def validate_jsonld(self, line, row): + import pdb; pdb.set_trace() try: - credtools.validate_json(row, self.json_schema) + check = credtools.validate_json(row, self.json_schema) + raise ValidationError("Not valid row") except Exception as e: msg = "line {}: {}".format(line+1, e) self.exception(msg) From bac9c7209a1ce2974b69caa902717b0797134d6c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 19 Dec 2023 10:31:44 +0100 Subject: [PATCH 06/70] fix requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b19238..a065a87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ python-decouple==3.8 jsonschema==4.19.1 pandas==2.1.1 requests==2.31.0 -didkit==0.3.2 +#didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 From d2f7e5395d73d4365ea8f87668834d4f2561b941 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 3 Jan 2024 17:52:46 +0100 Subject: [PATCH 07/70] encription from a env key and password admin --- idhub/admin/views.py | 2 +- idhub/models.py | 40 ++++++++++++++++++------------------ idhub/user/forms.py | 3 +-- idhub/user/views.py | 3 +-- idhub/views.py | 20 ++++++++++++++---- trustchain_idhub/settings.py | 2 ++ 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 1725339..f8fd6d0 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -645,7 +645,7 @@ class DidRegisterView(Credentials, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did(self.request.session) + form.instance.set_did() form.save() messages.success(self.request, _('DID created successfully')) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) diff --git a/idhub/models.py b/idhub/models.py index 6a326b9..6e6e18d 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -421,16 +421,16 @@ class DID(models.Model): null=True, ) - def get_key_material(self, session): - if "sensitive_data_encryption_key" not in session: - raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") - sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + def get_key_material(self): + if not settings.KEY_CREDENTIALS_CLEAN: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") + sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) return sb.decrypt(self._key_material) - def set_key_material(self, value, session): - if "sensitive_data_encryption_key" not in session: - raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") - sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + def set_key_material(self, value): + if not settings.KEY_CREDENTIALS_CLEAN: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") + sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) self._key_material = sb.encrypt(value) @property @@ -439,7 +439,7 @@ class DID(models.Model): return True return False - def set_did(self, session): + def set_did(self): """ Generates a new DID Controller Key and derives a DID from it. Because DID Controller Keys are stored encrypted using a User's Sensitive Data Encryption Key, @@ -447,7 +447,7 @@ class DID(models.Model): """ new_key_material = generate_did_controller_key() self.did = keydid_from_controller_key(new_key_material) - self.set_key_material(new_key_material, session) + self.set_key_material(new_key_material) # TODO: darmengo: esta funcion solo se llama desde un fichero que sube cosas a s3 (??) Preguntar a ver que hace. @@ -513,16 +513,16 @@ class VerificableCredential(models.Model): related_name='vcredentials', ) - def get_data(self, session): - if "sensitive_data_encryption_key" not in session: - raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") - sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + def get_data(self): + if not settings.KEY_CREDENTIALS_CLEAN: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") + sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) return sb.decrypt(self._data) - def set_data(self, value, session): - if "sensitive_data_encryption_key" not in session: - raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave de usuario.") - sb = secret.SecretBox(session["sensitive_data_encryption_key"]) + def set_data(self, value): + if not settings.KEY_CREDENTIALS_CLEAN: + raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") + sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) self._data = sb.encrypt(value) @property @@ -553,7 +553,7 @@ class VerificableCredential(models.Model): data = json.loads(self.csv_data).items() return data - def issue(self, did, session): + def issue(self, did): if self.status == self.Status.ISSUED: return @@ -562,7 +562,7 @@ class VerificableCredential(models.Model): self.issued_on = datetime.datetime.now().astimezone(pytz.utc) self.data = sign_credential( self.render(), - self.issuer_did.get_key_material(session) + self.issuer_did.get_key_material() ) def get_context(self): diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 3735d64..53a1149 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -18,7 +18,6 @@ class RequestCredentialForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) - self.session = kwargs.pop('session', None) super().__init__(*args, **kwargs) self.fields['did'].choices = [ (x.did, x.label) for x in DID.objects.filter(user=self.user) @@ -46,7 +45,7 @@ class RequestCredentialForm(forms.Form): did = did[0].did cred = cred[0] try: - cred.issue(did, self.session) + cred.issue(did) except Exception: return diff --git a/idhub/user/views.py b/idhub/user/views.py index d59f7d6..482b40e 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -128,7 +128,6 @@ class CredentialsRequestView(MyWallet, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user - kwargs['session'] = self.request.session return kwargs def form_valid(self, form): @@ -190,7 +189,7 @@ class DidRegisterView(MyWallet, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did(self.request.session) + form.instance.set_did() form.save() messages.success(self.request, _('DID created successfully')) diff --git a/idhub/views.py b/idhub/views.py index f8a62a7..e87e5d7 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,8 +1,10 @@ from django.urls import reverse_lazy +from django.conf import settings 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 from django.http import HttpResponseRedirect +from nacl import secret class LoginView(auth_views.LoginView): @@ -24,9 +26,19 @@ class LoginView(auth_views.LoginView): admin_dashboard = reverse_lazy('idhub:admin_dashboard') if self.extra_context['success_url'] == user_dashboard: self.extra_context['success_url'] = admin_dashboard + password = form.cleaned_data.get("password") + # Decrypt the user's sensitive data encryption key and store it in the session. + self.decript_key(user, password) + auth_login(self.request, user) - # Decrypt the user's sensitive data encryption key and store it in the session. - password = form.cleaned_data.get("password") # TODO: Is this right???????? - sensitive_data_encryption_key = user.decrypt_sensitive_data_encryption_key(password) - self.request.session["sensitive_data_encryption_key"] = sensitive_data_encryption_key return HttpResponseRedirect(self.extra_context['success_url']) + + def decript_key(self, user, password): + if not settings.KEY_CREDENTIALS: + return + + sb_key = user.derive_key_from_password(password) + sb = secret.SecretBox(sb_key) + data_decript = sb.decrypt(settings.KEY_CREDENTIALS) + settings.KEY_CREDENTIALS_CLEAN = data_decript + diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 61d9637..305c929 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -184,3 +184,5 @@ USE_I18N = True USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' +KEY_CREDENTIALS = config("KEY_CREDENTIALS") +KEY_CREDENTIALS_CLEAN = "" From c671ac489f2f3b159e878bf0f7c6a620c1ada721 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 3 Jan 2024 19:53:11 +0100 Subject: [PATCH 08/70] change settings for cache --- idhub/models.py | 21 +++++++++++++-------- idhub/views.py | 21 ++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 6e6e18d..05b163a 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -4,6 +4,7 @@ import requests import datetime 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 @@ -422,15 +423,17 @@ class DID(models.Model): ) def get_key_material(self): - if not settings.KEY_CREDENTIALS_CLEAN: + key_dids = cache.get("KEY_DIDS", {}) + if not key_dids.get(user.id): raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") - sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) + sb = secret.SecretBox(key_dids[user.id]) return sb.decrypt(self._key_material) def set_key_material(self, value): - if not settings.KEY_CREDENTIALS_CLEAN: + key_dids = cache.get("KEY_DIDS", {}) + if not key_dids.get(user.id): raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") - sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) + sb = secret.SecretBox(key_dids[user.id]) self._key_material = sb.encrypt(value) @property @@ -514,15 +517,17 @@ class VerificableCredential(models.Model): ) def get_data(self): - if not settings.KEY_CREDENTIALS_CLEAN: + key_dids = cache.get("KEY_DIDS", {}) + if not key_dids.get(user.id): raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") - sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) + sb = secret.SecretBox(key_dids[user.id]) return sb.decrypt(self._data) def set_data(self, value): - if not settings.KEY_CREDENTIALS_CLEAN: + key_dids = cache.get("KEY_DIDS", {}) + if not key_dids.get(user.id): raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") - sb = secret.SecretBox(settings.KEY_CREDENTIALS_CLEAN) + sb = secret.SecretBox(key_dids[user.id]) self._data = sb.encrypt(value) @property diff --git a/idhub/views.py b/idhub/views.py index e87e5d7..463479c 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,5 +1,5 @@ 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 @@ -26,19 +26,14 @@ class LoginView(auth_views.LoginView): admin_dashboard = reverse_lazy('idhub:admin_dashboard') if self.extra_context['success_url'] == user_dashboard: self.extra_context['success_url'] = admin_dashboard - password = form.cleaned_data.get("password") - # Decrypt the user's sensitive data encryption key and store it in the session. - self.decript_key(user, password) auth_login(self.request, user) + # Decrypt the user's sensitive data encryption key and store it in the session. + password = form.cleaned_data.get("password") + sensitive_data_encryption_key = user.decrypt_sensitive_data_encryption_key(password) + key_dids = cache.get("KEY_DIDS", {}) + key_dids[user.id] = sensitive_data_encryption_key + cache.set("KEY_DIDS", key_dids) + return HttpResponseRedirect(self.extra_context['success_url']) - def decript_key(self, user, password): - if not settings.KEY_CREDENTIALS: - return - - sb_key = user.derive_key_from_password(password) - sb = secret.SecretBox(sb_key) - data_decript = sb.decrypt(settings.KEY_CREDENTIALS) - settings.KEY_CREDENTIALS_CLEAN = data_decript - From 4225421147de10d45c9841701d515dbb02964a91 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 3 Jan 2024 19:54:10 +0100 Subject: [PATCH 09/70] fix settings --- trustchain_idhub/settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 305c929..61d9637 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -184,5 +184,3 @@ USE_I18N = True USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' -KEY_CREDENTIALS = config("KEY_CREDENTIALS") -KEY_CREDENTIALS_CLEAN = "" From 3655291dc6849a1d995d02809a3975334e9307e6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 3 Jan 2024 19:55:04 +0100 Subject: [PATCH 10/70] fix timeout --- idhub/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idhub/views.py b/idhub/views.py index 463479c..6d51159 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -33,7 +33,7 @@ class LoginView(auth_views.LoginView): sensitive_data_encryption_key = user.decrypt_sensitive_data_encryption_key(password) key_dids = cache.get("KEY_DIDS", {}) key_dids[user.id] = sensitive_data_encryption_key - cache.set("KEY_DIDS", key_dids) + cache.set("KEY_DIDS", key_dids, None) return HttpResponseRedirect(self.extra_context['success_url']) From f62348dcdb305a9e92db3a9b600773160102dec9 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 3 Jan 2024 20:14:04 +0100 Subject: [PATCH 11/70] fix perpetual key in cache --- idhub/views.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/idhub/views.py b/idhub/views.py index 6d51159..3db164f 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -21,19 +21,23 @@ class LoginView(auth_views.LoginView): def form_valid(self, form): user = form.get_user() + # Decrypt the user's sensitive data encryption key and store it in the session. + password = form.cleaned_data.get("password") + sensitive_data_encryption_key = user.decrypt_sensitive_data_encryption_key(password) + key_dids = cache.get("KEY_DIDS", {}) if not user.is_anonymous and user.is_admin: user_dashboard = reverse_lazy('idhub:user_dashboard') admin_dashboard = reverse_lazy('idhub:admin_dashboard') if self.extra_context['success_url'] == user_dashboard: self.extra_context['success_url'] = admin_dashboard + key_dids[user.id] = sensitive_data_encryption_key + cache.set("KEY_DIDS", key_dids, None) + else: + key_dids[user.id] = sensitive_data_encryption_key + cache.set("KEY_DIDS", key_dids) + auth_login(self.request, user) - # Decrypt the user's sensitive data encryption key and store it in the session. - password = form.cleaned_data.get("password") - sensitive_data_encryption_key = user.decrypt_sensitive_data_encryption_key(password) - key_dids = cache.get("KEY_DIDS", {}) - key_dids[user.id] = sensitive_data_encryption_key - cache.set("KEY_DIDS", key_dids, None) return HttpResponseRedirect(self.extra_context['success_url']) From bd84dbc3bb0e9772146e3b0cd79b5c29d74ee537 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 Jan 2024 12:43:24 +0100 Subject: [PATCH 12/70] encripted in reset password --- idhub/urls.py | 15 ++++++---- idhub/views.py | 14 +++++++-- idhub_auth/forms.py | 3 +- idhub_auth/models.py | 69 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/idhub/urls.py b/idhub/urls.py index 785f4d1..f7df70d 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 +from .views import LoginView, PasswordResetConfirmView from .admin import views as views_admin from .user import views as views_user @@ -44,13 +44,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/views.py b/idhub/views.py index 3db164f..8e7f542 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -4,7 +4,6 @@ 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 from django.http import HttpResponseRedirect -from nacl import secret class LoginView(auth_views.LoginView): @@ -23,7 +22,7 @@ class LoginView(auth_views.LoginView): user = form.get_user() # Decrypt the user's sensitive data encryption key and store it in the session. password = form.cleaned_data.get("password") - sensitive_data_encryption_key = user.decrypt_sensitive_data_encryption_key(password) + sensitive_data_encryption_key = user.decrypt_sensitive_data(password) key_dids = cache.get("KEY_DIDS", {}) if not user.is_anonymous and user.is_admin: user_dashboard = reverse_lazy('idhub:user_dashboard') @@ -41,3 +40,14 @@ class LoginView(auth_views.LoginView): 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) diff --git a/idhub_auth/forms.py b/idhub_auth/forms.py index f292182..c9ab1f6 100644 --- a/idhub_auth/forms.py +++ b/idhub_auth/forms.py @@ -2,7 +2,7 @@ import re from django import forms from django.utils.translation import gettext_lazy as _ -from idhub_auth.models import User +from idhub_auth.models import User, gen_salt class ProfileForm(forms.ModelForm): @@ -31,4 +31,3 @@ class ProfileForm(forms.ModelForm): return last_name - diff --git a/idhub_auth/models.py b/idhub_auth/models.py index cf1ac69..2fcf669 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -1,6 +1,9 @@ +import nacl +import base64 + from django.db import models +from django.core.cache import cache from django.contrib.auth.models import BaseUserManager, AbstractBaseUser -from nacl import secret, pwhash class UserManager(BaseUserManager): @@ -44,10 +47,8 @@ class User(AbstractBaseUser): is_admin = models.BooleanField(default=False) first_name = models.CharField(max_length=255, blank=True, null=True) last_name = models.CharField(max_length=255, blank=True, null=True) - # TODO: Hay que generar una clave aleatoria para cada usuario cuando se le da de alta en el sistema. - encrypted_sensitive_data_encryption_key = models.BinaryField(max_length=255) - # TODO: Hay que generar un salt aleatorio para cada usuario cuando se le da de alta en el sistema. - salt_of_sensitive_data_encryption_key = models.BinaryField(max_length=255) + encrypted_sensitive_data = models.CharField(max_length=255) + salt = models.CharField(max_length=255) objects = UserManager() @@ -92,14 +93,54 @@ class User(AbstractBaseUser): return ", ".join(set(roles)) def derive_key_from_password(self, password): - kdf = pwhash.argon2i.kdf # TODO: Move the KDF choice to SETTINGS.PY - ops = pwhash.argon2i.OPSLIMIT_INTERACTIVE # TODO: Move the KDF choice to SETTINGS.PY - mem = pwhash.argon2i.MEMLIMIT_INTERACTIVE # TODO: Move the KDF choice to SETTINGS.PY - salt = self.salt_of_sensitive_data_encryption_key - return kdf(secret.SecretBox.KEY_SIZE, password, salt, opslimit=ops, memlimit=mem) + kdf = nacl.pwhash.argon2i.kdf + ops = nacl.pwhash.argon2i.OPSLIMIT_INTERACTIVE + mem = nacl.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 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)) + key_dids = cache.get("KEY_DIDS", {}) + + if key_dids.get(user.id): + key = key_dids[user.id] + else: + self.set_salt() + + key_crypted = self.encrypt_sensitive_data(password, key) + self.encrypted_sensitive_data = base64.b64encode(key_crypted).decode('utf-8') - def decrypt_sensitive_data_encryption_key(self, password): - sb_key = self.derive_key_from_password(password) - sb = secret.SecretBox(sb_key) - return sb.decrypt(self.encrypted_sensitive_data_encryption_key) From e910f3ceec6ec0771285d2e6e963b0e7f647a985 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 Jan 2024 16:27:27 +0100 Subject: [PATCH 13/70] make migrations and fix some things --- idhub/management/commands/initial_datas.py | 7 ++- idhub/migrations/0001_initial.py | 69 +++++++++++----------- idhub_auth/forms.py | 2 +- idhub_auth/migrations/0001_initial.py | 4 +- idhub_auth/models.py | 15 ++--- 5 files changed, 53 insertions(+), 44 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index acdf6c7..fd644e9 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -31,12 +31,15 @@ class Command(BaseCommand): self.create_organizations(r[0].strip(), r[1].strip()) 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() 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() diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index b4d6ac7..5bd4f31 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-15 09:58 +# Generated by Django 4.2.5 on 2024-01-04 15:12 from django.conf import settings from django.db import migrations, models @@ -28,7 +28,7 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now=True)), ('label', models.CharField(max_length=50)), ('did', models.CharField(max_length=250)), - ('key_material', models.CharField(max_length=250)), + ('_key_material', models.BinaryField(max_length=250)), ( 'user', models.ForeignKey( @@ -169,7 +169,7 @@ class Migration(migrations.Migration): ('created_on', models.DateTimeField(auto_now=True)), ('issued_on', models.DateTimeField(null=True)), ('subject_did', models.CharField(max_length=250)), - ('data', models.TextField()), + ('_data', models.BinaryField()), ('csv_data', models.TextField()), ( 'status', @@ -274,36 +274,39 @@ class Migration(migrations.Migration): 'type', models.PositiveSmallIntegerField( choices=[ - (1, 'EV_USR_REGISTERED'), - (2, 'EV_USR_WELCOME'), - (3, 'EV_DATA_UPDATE_REQUESTED_BY_USER'), - (4, 'EV_DATA_UPDATE_REQUESTED'), - (5, 'EV_USR_UPDATED_BY_ADMIN'), - (6, 'EV_USR_UPDATED'), - (7, 'EV_USR_DELETED_BY_ADMIN'), - (8, 'EV_DID_CREATED_BY_USER'), - (9, 'EV_DID_CREATED'), - (10, 'EV_DID_DELETED'), - (11, 'EV_CREDENTIAL_DELETED_BY_ADMIN'), - (12, 'EV_CREDENTIAL_DELETED'), - (13, 'EV_CREDENTIAL_ISSUED_FOR_USER'), - (14, 'EV_CREDENTIAL_ISSUED'), - (15, 'EV_CREDENTIAL_PRESENTED_BY_USER'), - (16, 'EV_CREDENTIAL_PRESENTED'), - (17, 'EV_CREDENTIAL_ENABLED'), - (18, 'EV_CREDENTIAL_CAN_BE_REQUESTED'), - (19, 'EV_CREDENTIAL_REVOKED_BY_ADMIN'), - (20, 'EV_CREDENTIAL_REVOKED'), - (21, 'EV_ROLE_CREATED_BY_ADMIN'), - (22, 'EV_ROLE_MODIFIED_BY_ADMIN'), - (23, 'EV_ROLE_DELETED_BY_ADMIN'), - (24, 'EV_SERVICE_CREATED_BY_ADMIN'), - (25, 'EV_SERVICE_MODIFIED_BY_ADMIN'), - (26, 'EV_SERVICE_DELETED_BY_ADMIN'), - (27, 'EV_ORG_DID_CREATED_BY_ADMIN'), - (28, 'EV_ORG_DID_DELETED_BY_ADMIN'), - (29, 'EV_USR_DEACTIVATED_BY_ADMIN'), - (30, 'EV_USR_ACTIVATED_BY_ADMIN'), + (1, 'User registered'), + (2, 'User welcomed'), + (3, 'Data update requested by user'), + ( + 4, + 'Data update requested. Pending approval by administrator', + ), + (5, "User's data updated by admin"), + (6, 'Your data updated by admin'), + (7, 'User deactivated by admin'), + (8, 'DID created by user'), + (9, 'DID created'), + (10, 'DID deleted'), + (11, 'Credential deleted by user'), + (12, 'Credential deleted'), + (13, 'Credential issued for user'), + (14, 'Credential issued'), + (15, 'Credential presented by user'), + (16, 'Credential presented'), + (17, 'Credential enabled'), + (18, 'Credential available'), + (19, 'Credential revoked by admin'), + (20, 'Credential revoked'), + (21, 'Role created by admin'), + (22, 'Role modified by admin'), + (23, 'Role deleted by admin'), + (24, 'Service created by admin'), + (25, 'Service modified by admin'), + (26, 'Service deleted by admin'), + (27, 'Organisational DID created by admin'), + (28, 'Organisational DID deleted by admin'), + (29, 'User deactivated'), + (30, 'User activated'), ] ), ), diff --git a/idhub_auth/forms.py b/idhub_auth/forms.py index c9ab1f6..fa771d4 100644 --- a/idhub_auth/forms.py +++ b/idhub_auth/forms.py @@ -2,7 +2,7 @@ import re from django import forms from django.utils.translation import gettext_lazy as _ -from idhub_auth.models import User, gen_salt +from idhub_auth.models import User class ProfileForm(forms.ModelForm): diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index d40f0a4..5655a6f 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 2023-11-15 09:58 +# Generated by Django 4.2.5 on 2024-01-04 15:12 from django.db import migrations, models @@ -38,6 +38,8 @@ class Migration(migrations.Migration): ('is_admin', models.BooleanField(default=False)), ('first_name', models.CharField(blank=True, max_length=255, null=True)), ('last_name', models.CharField(blank=True, max_length=255, null=True)), + ('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 2fcf669..7c3434d 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -1,6 +1,7 @@ import nacl import base64 +from nacl import pwhash from django.db import models from django.core.cache import cache from django.contrib.auth.models import BaseUserManager, AbstractBaseUser @@ -93,9 +94,9 @@ class User(AbstractBaseUser): return ", ".join(set(roles)) def derive_key_from_password(self, password): - kdf = nacl.pwhash.argon2i.kdf - ops = nacl.pwhash.argon2i.OPSLIMIT_INTERACTIVE - mem = nacl.pwhash.argon2i.MEMLIMIT_INTERACTIVE + kdf = pwhash.argon2i.kdf + ops = pwhash.argon2i.OPSLIMIT_INTERACTIVE + mem = pwhash.argon2i.MEMLIMIT_INTERACTIVE return kdf( nacl.secret.SecretBox.KEY_SIZE, password, @@ -120,7 +121,7 @@ class User(AbstractBaseUser): if not isinstance(data, bytes): data = data.encode('utf-8') - return sb.encrypt(data).decode('utf-8') + return base64.b64encode(sb.encrypt(data)).decode('utf-8') def get_salt(self): return base64.b64decode(self.salt.encode('utf-8')) @@ -135,12 +136,12 @@ class User(AbstractBaseUser): key = base64.b64encode(nacl.utils.random(64)) key_dids = cache.get("KEY_DIDS", {}) - if key_dids.get(user.id): - key = key_dids[user.id] + if key_dids.get(self.id): + key = key_dids[self.id] else: self.set_salt() key_crypted = self.encrypt_sensitive_data(password, key) - self.encrypted_sensitive_data = base64.b64encode(key_crypted).decode('utf-8') + self.encrypted_sensitive_data = key_crypted From 10c6d20a10397f9f51a8adf17044de11c2991875 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 Jan 2024 19:17:18 +0100 Subject: [PATCH 14/70] fix command initial datas --- idhub/management/commands/initial_datas.py | 8 ++++++++ idhub/migrations/0001_initial.py | 4 ++-- idhub/models.py | 22 +++++++++------------- idhub_auth/migrations/0001_initial.py | 2 +- idhub_auth/models.py | 9 ++++++--- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 62c048a..8481a81 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 @@ -43,6 +44,9 @@ class Command(BaseCommand): 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) def create_users(self, email, password): @@ -50,6 +54,10 @@ class Command(BaseCommand): u.set_password(password) u.set_encrypted_sensitive_data(password) u.save() + key_dids = cache.get("KEY_DIDS", {}) + key = u.decrypt_sensitive_data(password) + key_dids.update({u.id: key}) + cache.set("KEY_DIDS", key_dids) def create_organizations(self, name, url): diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 751af85..05fdbf4 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-04 16:59 +# Generated by Django 4.2.5 on 2024-01-04 18:09 from django.conf import settings from django.db import migrations, models @@ -28,7 +28,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.BinaryField(max_length=250)), + ('key_material', models.CharField(max_length=255)), ( 'user', models.ForeignKey( diff --git a/idhub/models.py b/idhub/models.py index 30ad551..116f46c 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -412,9 +412,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"}' - # CHANGED: `key_material` to `_key_material`, datatype from CharField to BinaryField and the key is now stored encrypted. - key_material = None - _key_material = models.BinaryField(max_length=250) + key_material = models.CharField(max_length=255) user = models.ForeignKey( User, on_delete=models.CASCADE, @@ -423,18 +421,16 @@ class DID(models.Model): ) def get_key_material(self): - key_dids = cache.get("KEY_DIDS", {}) - if not key_dids.get(user.id): - raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") - sb = secret.SecretBox(key_dids[user.id]) - return sb.decrypt(self._key_material) + return self.user.decrypt_data(self.key_material) def set_key_material(self, value): - key_dids = cache.get("KEY_DIDS", {}) - if not key_dids.get(user.id): - raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.") - sb = secret.SecretBox(key_dids[user.id]) - self._key_material = sb.encrypt(value) + self.key_material = self.user.encrypt_data(value) + + def get_data(self): + return self.user.decrypt_data(self.data) + + def set_data(self, value): + self.data = self.user.encrypt_data(value) @property def is_organization_did(self): diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 8ea6578..ee16760 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-04 16:59 +# Generated by Django 4.2.5 on 2024-01-04 18:09 from django.db import migrations, models diff --git a/idhub_auth/models.py b/idhub_auth/models.py index aed2199..86da431 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -148,12 +148,13 @@ class User(AbstractBaseUser): def encrypt_data(self, data): sb = self.get_secret_box() value = base64.b64encode(data.encode('utf-8')) - return sb.encrypt(data) + value_enc = sb.encrypt(data.encode('utf-8')) + return base64.b64encode(value_enc).decode('utf-8') def decrypt_data(self, data): sb = self.get_secret_box() value = base64.b64decode(data.encode('utf-8')) - return sb.decrypt(data) + return sb.decrypt(value).decode('utf-8') def get_secret_box(self): key_dids = cache.get("KEY_DIDS", {}) @@ -162,4 +163,6 @@ class User(AbstractBaseUser): err += "data without having the key." raise Exception(_(err)) - return secret.SecretBox(key_dids[self.id]) + pw = base64.b64decode(key_dids[self.id].encode('utf-8')) + sb_key = self.derive_key_from_password(pw) + return nacl.secret.SecretBox(sb_key) From 5dc1577d9ee3d7e681429ae774fe3bf4d0ce5fb4 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 Jan 2024 21:11:11 +0100 Subject: [PATCH 15/70] encripted y download credential --- idhub/models.py | 7 ++++--- idhub/user/views.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 116f46c..c0e237a 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -3,7 +3,6 @@ import pytz import datetime 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 @@ -541,13 +540,15 @@ class VerificableCredential(models.Model): if self.status == self.Status.ISSUED: return - self.status = self.Status.ISSUED + # self.status = self.Status.ISSUED + import pdb; pdb.set_trace() self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) - self.data = sign_credential( + data = sign_credential( self.render(), self.issuer_did.get_key_material() ) + self.data = self.user.encrypt_data(data) def get_context(self): d = json.loads(self.csv_data) diff --git a/idhub/user/views.py b/idhub/user/views.py index e6e28dc..0a273ec 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -120,7 +120,7 @@ class CredentialJsonView(MyWallet, TemplateView): pk=pk, user=self.request.user ) - response = HttpResponse(self.object.data, content_type="application/json") + response = HttpResponse(self.object.get_data(), content_type="application/json") response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json") return response From cb9ef0b608c1067a89b0c5a70342f5b74adbbecf Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Sat, 6 Jan 2024 19:18:59 +0100 Subject: [PATCH 16/70] fix issue dids and credentials --- idhub/admin/views.py | 4 +- idhub/management/commands/initial_datas.py | 15 +++----- idhub/models.py | 44 +++++++++------------- idhub/user/forms.py | 4 +- idhub/user/views.py | 25 +++++++++++- idhub/views.py | 16 ++++---- idhub_auth/models.py | 18 +++------ trustchain_idhub/settings.py | 1 + 8 files changed, 68 insertions(+), 59 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index b6dcbc8..634ec0b 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 8481a81..0d8e0a2 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -37,7 +37,6 @@ 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): @@ -47,6 +46,7 @@ class Command(BaseCommand): 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): @@ -54,10 +54,8 @@ class Command(BaseCommand): u.set_password(password) u.set_encrypted_sensitive_data(password) u.save() - key_dids = cache.get("KEY_DIDS", {}) key = u.decrypt_sensitive_data(password) - key_dids.update({u.id: key}) - cache.set("KEY_DIDS", key_dids) + self.create_defaults_dids(u, key) def create_organizations(self, name, url): @@ -73,11 +71,10 @@ class Command(BaseCommand): org1.save() org2.save() - def create_defaults_dids(self): - for u in User.objects.all(): - did = DID(label="Default", user=u) - did.set_did() - did.save() + def create_defaults_dids(self, u, password): + did = DID(label="Default", user=u) + did.set_did(password) + did.save() def create_schemas(self): schemas_files = os.listdir(settings.SCHEMAS_DIR) diff --git a/idhub/models.py b/idhub/models.py index c0e237a..dee615f 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -3,6 +3,7 @@ import pytz import datetime 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 @@ -419,25 +420,19 @@ class DID(models.Model): null=True, ) - def get_key_material(self): - return self.user.decrypt_data(self.key_material) + def get_key_material(self, password): + return self.user.decrypt_data(self.key_material, password) - def set_key_material(self, value): - self.key_material = self.user.encrypt_data(value) + def set_key_material(self, value, password): + self.key_material = self.user.encrypt_data(value, password) - def get_data(self): - return self.user.decrypt_data(self.data) - - def set_data(self, value): - self.data = self.user.encrypt_data(value) - @property def is_organization_did(self): if not self.user: return True return False - def set_did(self): + def set_did(self, password): """ Generates a new DID Controller Key and derives a DID from it. Because DID Controller Keys are stored encrypted using a User's Sensitive Data Encryption Key, @@ -445,12 +440,7 @@ class DID(models.Model): """ new_key_material = generate_did_controller_key() self.did = keydid_from_controller_key(new_key_material) - self.set_key_material(new_key_material) - - - # TODO: darmengo: esta funcion solo se llama desde un fichero que sube cosas a s3 (??) Preguntar a ver que hace. - def get_key_deprecated(self): - return json.loads(self.key_material) + self.set_key_material(new_key_material, password) class Schemas(models.Model): @@ -514,11 +504,13 @@ class VerificableCredential(models.Model): related_name='vcredentials', ) - def get_data(self): - return self.user.decrypt_data(self.data) + def get_data(self, password): + if not self.data: + return "" + return self.user.decrypt_data(self.data, password) - def set_data(self, value): - self.data = self.user.encrypt_data(value) + def set_data(self, value, password): + self.data = self.user.encrypt_data(value, password) def type(self): return self.schema.type @@ -536,19 +528,19 @@ 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 - import pdb; pdb.set_trace() + self.status = self.Status.ISSUED self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) + issuer_pass = cache.get("KEY_DIDS") data = sign_credential( self.render(), - self.issuer_did.get_key_material() + self.issuer_did.get_key_material(issuer_pass) ) - self.data = self.user.encrypt_data(data) + self.data = self.user.encrypt_data(data, password) def get_context(self): d = json.loads(self.csv_data) diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 5ac04ad..efe9ad4 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -22,6 +22,7 @@ class RequestCredentialForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', 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) @@ -49,7 +50,8 @@ class RequestCredentialForm(forms.Form): did = did[0] cred = cred[0] 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 0a273ec..dc83377 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.get_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 @@ -135,6 +143,15 @@ class CredentialsRequestView(MyWallet, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + 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): @@ -205,7 +222,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 8e7f542..176449b 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -20,23 +20,23 @@ class LoginView(auth_views.LoginView): def form_valid(self, form): user = form.get_user() - # Decrypt the user's sensitive data encryption key and store it in the session. password = form.cleaned_data.get("password") + auth_login(self.request, user) + sensitive_data_encryption_key = user.decrypt_sensitive_data(password) - key_dids = cache.get("KEY_DIDS", {}) + if not user.is_anonymous and user.is_admin: user_dashboard = reverse_lazy('idhub:user_dashboard') admin_dashboard = reverse_lazy('idhub:admin_dashboard') if self.extra_context['success_url'] == user_dashboard: self.extra_context['success_url'] = admin_dashboard - key_dids[user.id] = sensitive_data_encryption_key - cache.set("KEY_DIDS", key_dids, None) - else: - key_dids[user.id] = sensitive_data_encryption_key - cache.set("KEY_DIDS", key_dids) + cache.set("KEY_DIDS", sensitive_data_encryption_key, None) - auth_login(self.request, user) + 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']) diff --git a/idhub_auth/models.py b/idhub_auth/models.py index 86da431..189b421 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -145,24 +145,18 @@ class User(AbstractBaseUser): key_crypted = self.encrypt_sensitive_data(password, key) self.encrypted_sensitive_data = key_crypted - def encrypt_data(self, data): - sb = self.get_secret_box() + def encrypt_data(self, data, password): + sb = self.get_secret_box(password) value = base64.b64encode(data.encode('utf-8')) value_enc = sb.encrypt(data.encode('utf-8')) return base64.b64encode(value_enc).decode('utf-8') - def decrypt_data(self, data): - sb = self.get_secret_box() + def decrypt_data(self, data, password): + 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): - key_dids = cache.get("KEY_DIDS", {}) - if not key_dids.get(self.id): - err = "An attempt is made to access encrypted " - err += "data without having the key." - raise Exception(_(err)) - - pw = base64.b64decode(key_dids[self.id].encode('utf-8')) + def get_secret_box(self, password): + pw = base64.b64decode(password.encode('utf-8')) sb_key = self.derive_key_from_password(pw) return nacl.secret.SecretBox(sb_key) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index b9eecc5..01567c6 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/ From 0ec40f66ebf079d0d2a16da6cdd0d48f7c11a3c5 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 8 Jan 2024 20:11:41 +0100 Subject: [PATCH 17/70] add sign of pdf --- examples/signerDNIe004.pfx | Bin 0 -> 3694 bytes ...Model_Certificat_html_58d7f7eeb828cf29.jpg | Bin 0 -> 16198 bytes ...Model_Certificat_html_7a0214c6fc8f2309.jpg | Bin 0 -> 56367 bytes idhub/user/views.py | 110 ++++++++++++++++++ requirements.txt | 10 ++ 5 files changed, 120 insertions(+) create mode 100644 examples/signerDNIe004.pfx create mode 100644 idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg create mode 100644 idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg diff --git a/examples/signerDNIe004.pfx b/examples/signerDNIe004.pfx new file mode 100644 index 0000000000000000000000000000000000000000..a141358ad278f1743aa6ee8509bef319e2010a40 GIT binary patch literal 3694 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 ZU|q+MQmaBY_B&q_@VZ&uTYnAve*w@4p#A^= literal 0 HcmV?d00001 diff --git a/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg b/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a285a9d11cb072745863432d32ab9b826e57f3b GIT binary patch literal 16198 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 Lw)S_Obi)4tI^#pC literal 0 HcmV?d00001 diff --git a/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg b/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b954dd560c310a769c91d788577a73174abafed GIT binary patch literal 56367 zcmeFYcU+Upwm%%jf*mZVsHh;efaxJYLDNZx^h5>I6ViJ@#oiGWMN#a13nEIfq1)cT z0t$$Vy&xhMih{r!aPPC-<+S@b_x|2L&I5#pnKj>;Rc2<*T0>iT+Y^8fg-9j>I(6;@ zNcDUH+G+qH(JF)0U{M*2qhOQ302G!?_o@N3KR)dgfKHyTUw*eeowz||wHm`gAiZUh zSZ0vOCrJ%DkW*{~K_-DgfGJa*MzK^Qw~ms?m1=!NVC|{1fuq#2h(JDq4yGG%a+NyD zZI-j$435;Tk)maRQzLs$afUl}MxER$9_7?&^_FmFL|_MaxaYmS7!)|FgT$(d2*kAa zI!Zu~9)&ZQ<)aXjV4+el1TqSVoCJX*kVr_#C@2^L1%W-^$WRD89DxjnhK>5=35@LN zd7EOEDZ*KJ;xFBK{)!0vg)fK0G06d&WH2j15HuRy4g-aTdMH9IF1=Ok4AonLzJvUV zgO^*RX0_3(Ht0vS8XZu)U+dJ>U6~N&nW5(Pq|m^dggjrN( z5y*~lwQKX2qTfdK=SC{ITB))|!r{L)`laX}wfbd_QKAW?-`f3B_>bCkOv*?QIvxba zU}D%B5rw71U@=MpMu@>GF&I23ECyrYl2mpX3GEh>&<--5Zlhx`@EDDjfdPUs9S;`c zp>KZ(n6W`l%w$YVdt=WNgTaD;a135U)JUKdEr!JDD8?v25)9c>sf;0DF^_7cnnzR=|M0!!E`_V0~v9 zLuO%O=qwLw1dN#6&Sn&*ox2zh(chjJBE~@`dBhTh!)QG0dFqs8{2%GQ1IA!*9&SBy zk&*PCQ z)X~@uK&~}PMR$tSC>+bBv=g*3ZWc#lqPm2zXdHtZqt_srZiY(66-9CIU>liCG0-VE zl~SalctmGngLzg4kBT+B;Gk$i2OxyW0gD8DqE^g@IAsDYnlFN?jAEVDBH<(KLKq7o z)63v;fiX-G1pz8xOoB=%j#86#R5jeih&KRn8U&lCHOhrLQ3s$&qNhORdIUAzNagE{ zNVU;qvRTYxq}{;6V8|Z!$#`sx$6R8B1f0hpB<-?Q5%5?XflcQUh+Gy!MOKQON)f@; z0cem!If(Qq7M#t%5qT7~)J||y@GQJiC1!J7e6=u)hQq?5*$g>_pmfT(8i%rjWlFq= zNK(r*G9nj-GBMo_m(th)2nI&GRTQ?yO6PD9(JZY@$mba)0wP2%fUvY8oy;KS8_g0D z#3q6MGxj?Gfp{&2&(|WpRcA6GT~@ORXa|WshUPI7435xl zVt5n=&EgY5BBV;>6lk?{3))O)aytOIE{VxuW*dNEM6L=>T*)SQ?HFtHCf-2%tEIrO-whG)gpwM}wjuxGDN3Qivn7HKK#n<{rvU3EVnMWA4F%hgCN(9JD~d5z>fiC7tTjs;Cq=M5?n!*h3MTp zgc%HS84*s1osS4JO5AR(1PW%VHB_1yh9fY|EHKQ3aS9MJ85+goAl)dSLWeLb^wC@y z2dw6BjRrIw<>&y!IW=Gx4QXRA*$x(zWe|~JA`zYmg*mw>G094$S|A`g&22?!IBtdv z%JaC?P?=d|A;aNtK9vB*P(*4f0TK-sxu6|@c8E>jV{fvPAm8>S))X)L}5O^88t0IKYEE`ed! z!Ncefg2hHfNF8Ds7V0(%g;Gcif*=Uf2!KMl%?;#R8DJ2{8Vyk(?Q%TQX%d(OV5E*M zhgpc|7%0rd?f^t{XbcK5%5Ha>bq*OXmQBD_&B;=9cSSHnA z^r(o2<>QHXrxXJ*h%w-3rI;WzsiMIMgWb$>D2zBLTFjT3*;E?jJ0OrFAc#>g6;BMI zt4&m`+(4olC?F7tOGW7)-DnE=Os#XeKOPO>?mg8|Vzeg%c;Sp!*ei4ZPf()f5f+D(+QU>r3Kq5(4560lZ-rirb5AW4Uo z+o)zU2W5_S@oh4>7-R{fg0&rh5T4nNM5!GDsSz$<*pN22)5&t7cm_Ak&Bf{229Mk! zp3Z1^ltHLq5bd71BX=mRR*}}m)2eNF27?d}qgvgB`0u`r0qN8#p$MT`AFt&LqVZ84 zrCV(dq=KTcE3sO(0*OH2Q5JES42QPUq&hemBNkI!dOAiTHA$i*c3rd{OrlZcs1b7N1iW*O&dX_Sv zP$IF3MIt$kIawU`a33k)DWO%2!EMEEE- zUCWe)xuRq`vo#EFz;Y;37E}imFo<9`Qb!{}(FCzV(lJ7hL+OZ;s+lm6Mgn$_gkijR zzE-ZK;w40`>&Myn>xBJ%nNenzt3gHIsl$)^mt=+V!emAh&qNdQofG*i0Em|pziU)?7a9o2MqoK2HP|p}OAPUU|1{z=_ z0|{gY33M?mRv6Sspkna^EYW3kIY3eySjPUo9?B^o9n8s#ms{-!tVLyJs$mp}EFK4P zU zW|b(_Hn_w+v-ZG449V-K?;l~^6lgypd9E(h3!0`sGhRJRT@iISD8Q$A$<~h{MdR zD6T>))nl-5g}{!GKv2<8vI*xv*~LziPH&U|1!k8UVWTNw7KvOhVd_a8BNQ=dOtD2y z=c3d~Dwb{F(y>mr*rpMXpeUnNET$?^RsqJMqXRW87LJMJvvCxJRz*Qlc`6x2Ad9Cs zfj9%m#WsqB9e~kfm{!Q3!yr_%+a?l0=@zF5CX~uK1Upw7!*PPNCNbBn!=-@nJfl$O7a1u<`zW9{;JL1CS@8 zgVhcjSgb-2xmp6n;MqA;TnxzMsSmxLF1!90KCffMAVXqDPs8B0d|YRe>EazTRa)Td_n5i}ii#LFIJ5 zMH8k%h$sY>5X|C4!SR|HlTIVI@wqUq3GDICP-d6S#MkPgSzsI6p@6{kY8^q!uwqtfvS}Pnw4126(s)EW znMNh@;>9eC11(l^EnK0R%HokFYP-WCQwgAMA=AubM1kEXr-Ti+xD^73xkH9BK1%GM z$V`+dibx3dY<$Wn6-}xZ#79$%R11$n!H{`8u0aVFprB@V3<2%&vLq57MWoT|`2ux3 z6)Xkgu{<%6qQP|lqR2W7kxEcVurvsZr-dr{dL5gJu#2o>y-G_^@g(#Zx)T%6Ly&-; zGp3LWvSVxznUbnOOL-hUQ6Myh{rb@X_}icV^o>sd%FrYb!!uXdB#tW{p|qeWU>KMe zZxc#%Fuu40FovyyiHuwt%MOXcGARry59ks{5xF*w%MKKWiQRaGKxaZqq%0E5O3-?C zOCADlH0YEX8&9ogV_eY^H`C%kGt`XlW@AIZ1Vk>zuBD;Scu%0g1QbbP1U#t4sE_v? zE0k;!iHajQ+|lA_BvoO9YGgvV3dJF*h+-9wZjgCgNvDE}CN@Votz?JILN=3P*ftVH zXo4AIJPun2AeSrEdQN^enOlXh@VN+~j&4#ig&>c8TNu$g2g~7(XEG3YG{sXyqg&ij zfEd+_$pvCjWwK$}kg$B}8B4)j-1 zZHtv9|1DMVm%(sLdx#bEYlPK99`x5h1iL3k@sEPzzXu}NJqe265+Cge1do*H^l-A? z;+cO^IT;`6DVU^I%fi73lqVq*1_y^h!AKMYf`URZI2a6zf*>$>1TL&2#QOWDziI0i zSiC`MYtM=N0xSDBV1H=(Cs?Lg?MX|CwLj3Q+|OT=R#5HtjiK_Sp! zJPd*Wqj0EShx`}q|I`Odu9vCXvq3nm*kXw^nhgd|^joI*72^-}e`=4Ho7Hx?jA%CK zI#MJ5Ja9gx!!E+rzp0{wDF|EwlPbc%p9*P9R5up;4!jlt2%V1EX0vaZkqNjlV z)b_UlwQKo%wd~L82!p^07%Y|m#r&paEHMm)LBJ3g7!>g<@()e_3fb^K*2wpf{YCq~ z>c;$cmGT|t5B0x6{HnDdwDLpp;}>gOv6xRMKme`K$nkstK&53-v-Q2l+7-;4iW$T0q&YVEr&e?J9( zozH)mvj4HoUHw~r^{@BW9|$_`O#GMxo$^CvINK^VTW!W@vq7Qur1hiCo;2+g&_AH~ zMNR)PRy=#wU$fw;@C+UH`^>qgFx)d%nN2D;|1y80JvS`0YKd8Fc8!97CxIbTK)<8< zhXel3hUb!o=Y9z2Zx=-Vw3z;~)ct3F{e{8*XFF&|`)?=zmQ4REuK$Yb-;%(;Mf_ju z`mebDEeZTv#Q(Lf|CqRX{&vOGbEJz1ba*b1w!H`R?eIMTvY*>8thPM>ECO`;*PqV6 zJzjr%erevhQzuWY=jU$~|LRNI&j4R-m`1BUR^p$f2sVn4&dFRTX(N6T|0N`=>q_C^8$48^6K2#L*2bAptGmETX#Sw z?;gJW2aFy#$ggMMU+ zxl7kh-MV>sM$xIWmv5J@{kwG^4e`bdh!F=4G|h)%gCs07Zp@$_YZt78NmH`%euLX8 z0KGl*oqfH00aF2wpLd$~^DOX?-X4q}U+{;Q)!cNvV)dho={@5nbrL}$Ts}Se_c)sD zgo{P23elc7<&-G}%_|W_*g$$|{yU%ix+40EsmL#%FI)2>pH^OGh=K$ay1E-Dwr6Of za`*#j?n}qwM(A3O!QMQY3;NRhvR~+n!?ES%jL7~d&$S6!VNd0}BIU-z|C5uQJIS;NY*Nh73XyCOF{M!#e1&AzOES$1#pk%f7R zBAqTJD+!u*Y>pL_FmRUk-TBhof;Zt`A^;<5F?FSlnO9a@3L{zXC(}NCYaBP*UM6BA zjy&I*I9#9%nEJG7PLLqkx})ZC*DsD-jqiE;W8dUpYw=fP-K|vkH<&(XQrz>V7A1MB zVsya6nKLQ}F4vq|*T`K^S{JMb4xGda)t=i@;9Wn~UtfQW?vr2NdL;jBRpsY6y<>Xj ztGI$w*HR{_TdON{2G{lk+O5-tZGfSvH!I#XB=nBUFMoDrX_rJDP{gkYs>t@O@eQcF z<6Ad-QF^?EGG!rc%qLo>i|J7pg?s%4Inbi_>p9~AP;IE;0E0ib*Ah2=vcyt4L}#HMZR^I~orpr)Gl5QMm{ zJ0vUx(r>Evv`pnip4hva9uX=`j;gDhHM@4jY30MZTvw~t(nq2@dmfK1>GC2;8CSA# z7nEm9tIymuM_E?NzP{&kr@PTO*4`tX1|GlPYlw9Rs7U?1ziwaOg*(=*!1*zjcT!Yi z+5oG06~`s@m6xi|9?n0jo_*%i(ysB6_udtjVdJ;Qor=1!g?avjk)RpSnz6}T*kU8~ zZTNzI+WLI&XM16-a}ris*OH$4P}g*JnE0WnaZ?+BG3ARSax?b%*OF_u5RaGUlgdty zWZ&Yi%6Un6y?-p_;gg&@Z;qCLsn>l34~eFQ-aRG|Z)Z$7mq2BJsvchcSa$8~Fh|YD zk6%AeU;g?i4L|DolVMq`J$m5SY)q6 zZDBlD`sG$dB`NObm9)m=@wAG&&BqT4O7D-BoCu6D{?$DH^UVLhD2WUk?ANfQ{^rda zYjbmRce9?oc=2q{dU{RKo8|r+R&ESoK~we+*P^lD5}MC6fme1fcWM6Vrw{eWW_m$m z!l@F!vO%E+=4 zjRmiOB3AD5+)NFPpz(#lAie{jWtP0zMG;dOO!)^SWBeo_9fAV_dV|(o{{sJ8zMULnG59Y`S|X_`Ux!O64UGSS$U&-W6KwX zHr38O(fydB==crYA4`EE zUSMvf1iqp)dk-G(#^cq#?@~Ead)3{-r!K$VeP$+Z!1N>DCmvoby0j~Z0a-Lmu-N>m z;Y15WGWeq|8~RjJ;%;cFb{A!rZ|nU@xfV#j<#i=Co!*i^eR=b+IT0rhX-GUla@PHQ zwIS%+@{gdESt;s=>HDUnmqys`*GLEYP-uS3y_2-CX{7xB!O+u?-|zSdNKtFK!+^IjXkIQ#Ru@`RxLgF_^VjF}Ctw4AvK zb58SsQ!iNNB!(i=ST~+lCyQ>3>9y?Go(1F2FM(~__i~M7&Bk|)>+HKTGR7=A)^e&* zd&DmyA-~3F?zSy#`G+>Zg=SIdiRx?b6=TOL$G!?wGWWEkJ9m?ts(d5Qqy%hM7qkIx z8ql>MjjraRF7OV-ccmIOsBGrg$krJp8FRw--Kp}w*i?P}q;uu-aeeyL1X)t2nnwEN zl|?uv&w|}eXx=eb1@gb&bUQZ-!;KY>W|KbLd>)sZ_jp!I_}rvYa&cls z8(`eS`qruYOCCIqO}+K%^)`FOoSMfYju(4sQj{wY$;^^gP;S*Xcz#A}mM$o#`FyD4 z^s%B0z3ZZ!(Q?@*RaR0!s$g^^te%;#Ne%W2| z=vZNG-ie53r_!DIgIfKcx8&}c+M~F{x7IK9PEA(xg2uTO_4^&hDfZgIIS`X>CM9NtfqtE)2eY_Q!lOGgP8z& z2E0r1UbKR}cE?Id*!?W~bJFbDl22Lk=hq7M6{qhXlq^g?H6cB3MJw_g;;^$gLWj8G zE+eO%*>+I<{PD(ZLu=n1Zx~$p;_27)6yG6^`osPej~6=z+zGa8Wqlr&}j)FTjN(tGxO)?(Bl8%ssMfa@oFm&zisXy+i|>j=!{Pj(FUm_=@cK zLb?L6JXyVXyJ>$46}R;s{CtMxQOH|a=iqVA{U#KrSwqL2oz}c;U=?fd*_j9+ ztR~M#)n&-WyV!wSKRmv+`t`?GGiHEH4?yGYXG3CFGEODsCXP5mWxmanwss@GDGr1@ z7@Sdv`+Vi%w%ebU3~xDCk$d5ycxCpASkBgzIGKb%`IuDX4PRJ(8Otx~Pz%?b6LxbXeW;`fs-ziu3H z?9ucYry@0;yyfbTb*&GI?_?TQe{4FqE(Kno7<&v{n^p_C@H;p0|6;TFlW*G5sn^RH ziwA5DPC6V_d^`*l9KG1x_1bmba&xe}Pxu~9{{x*CO`z;q*u|^Y5PYY3)7u>w)UhWQ zPF|Rk_iDqn{AL1WEx4XQf*px>3`O;h?A?JZwGp|!?$2`R}BL?2IGYckaupC3LJ zj(6*;eVejbV=Zj}o`2E4^6Ky}wU_8R=2&fd>@`s+aO$jJVMtF(aZ|(a-P3!_>&$zJ zpVxn*Ijqiq?~eY_y-J>p3hb5~Q2(LvlK#QFZ-Nsyn+^xCPt>g>PFS6}uX4^aAQeC7 z+uHgwQJe8?03vGB%0lbs{e_3CYcuYjO1Q$mb)jL&F=3JLP38MTH84%7vf<1As^o!} z$;mB+DSqj;d@!Kvyx>%yc>wQe06@4m08owb#`;u?ym6Usji^Z@41E@uJt#vhXjnAH zvoq5(VaT*n#hh1h87*_$wcfd#ZE3QbbYKnY^pNt+XL6UsEE#tuL7>oe z?J-d;!mM=PE(8;@09c>YJ%G}mL;JlZRDJCtr=#oW2mXJon7@<5xx? zZV8(T3+2m}t$494X)NhW+>6I+W=}nFDf{sBPjk}zmYum0@k&E!(oCLl?4B-nca`YU zk;1~FY*ER*fpWex^NkSEbmrg@QNJbUo?YH&N~!44sWH23*|WvnyY_uTWsL7qFy&0@ zCU-~*pi{ovn@S0pb!RN*Oxc(07n7K0kJ_`0;=Ef6D+=R~4QCJarNLk&&C_Xk*|pR_ zYsN*!NMVtvkPs?~c=|c$@)Ty0sd1xLdAJR5+_51S`EAbr-H{<51=TGt0~IF81?1{O zJO!+YSlN(qlk%>G$6X_e?RjAazxM=bcGTOMPZxd&TMEI2h6XqX8UG^VvyS5`J&bG7gba<|Z`CoZWC z`1pQO%lptjKUFRtA zkGiEt)+EzvYwVwn4oe+cP#qVzeo|Le_~V_iry5le{Z1#?;wzs&yJ@TD4+#`q)78Z8 z*>Yh1u9RUA>M;MMgZ)~9#zxJaJ4v&N*16A!w`0b<%S`*&l=ki`alp9mYjA)uvF3FB z)K=2hHb5J|CnN4_?>?7i1Hbxhc9EZ!PRiQOhq#YoYh{9HOQ5McUzsXlT^Qa!h;qDF z?1Dk(qjM(OgS-k7a0PR~q1*p|u&Tp5?>PH7{$%3Q>^8vNIorJwAGWKg$CMRuU2YGb zdcOfS@I=y*>xa?(c4b@*3}(LZ-?n-ke&U2v?`BftEzF)DH%XhpfT-&s3HNtYF3emR zyD4D!!Xc}CmO#9vL-HW263Q1rdU-xZ8$2JOwPgv9%MyOB3;Sqj)}AI-rVlOL_?mRz zZ{Xo4rzfg1cYhlcaMQ!o#SYfR9xWBB1boI4?3N#hBVL*g=rSr~ zdimuMn(6{gXYIUM;7;J|F0+PU!M!`*o(JwF^1=IdPq@7x)KE6Pti=6u%Pi?+QPuwO z%d$(hT)v-Cb%13-l0Wwnx%>`4yEtsu68rUV49KQbhZOY}EUmd;@i=Gl<2sFV$ckO> z&jh|1I_c?pkn~gLBXE!K=<$0Aw{u6N1Q*X~Zt3@6ATqYDYM0~V$}eX?h7YyXb(P&0 zFX?`$tfo&Jpj&xdJY{vW>_15lp&h%=XUzUw%o*{iOV?E|&+nV$|LIQ8 zz?EV6A@SLm7tGBxp|Wz1F z#)T$UUJ>S2w*fY1*GyfM$J99;*S9{+OKQ~F+W^2*<)W#K_?l0|VRI50(=VAGeBqTq zGUh_EW+d77kG(Nt+TAO+xu(P2-i;m|{uS#NaS)tPIgi2~*`&0fm=Q6)zQz?e~2_-+Ht5Za3FT*gV`%5rFM!LPa3?+x&ht>2<&-pOdpn2B9BiyWbV1U zI5h|md%bblk$0lf`pnwSGXVeGm~Cx508}-_)lvz7EJip1dX2t@bXwKk+t-BDj-D(dTBq+uMV4 zIOJ3B()`S3{0Kw!{Euw_Wc=ASz)_Fp+W;=o)asCaZ|M642jIKBv!5upx#o>oN8Wg) z5dNIzH&00L>+ak8Lhm8DJC=$(VTdM;*O|WvxBl<>(WQ!}4RD}2vfDSP zmC1-X*vqPWURSp~Vc5~JL%UauiTe_^H)^0y>Bk0O$suh1$CXQjZ2;d({xREbseT?+ z>n>M)i>q1nY3aka=fXZ^zC6i$n_EOo=iNh;Ra4F3<&=`X5H=2C9@n%hD~mS%SmLrJ zf|I4U{UM7Adi&p5*n8x?sk!~1Jvphcl(wQ0N~>EPcOZ@j6Y$aW{iVg%v~ItOeNpdL zcb}Z`LOnP*bu6|*wKi`ecIV{m2X}^{BO9~UAjh_xtYj$8Fux^+F%NTf)hGL}OGgzu z2AmvtaElEgD!zJU^a>$Pi5Es~8AuCaUh+HMk|I9;L}^CN2|7O5gLN27R%L!>(=7MFhO=wBjqD#8ytAOIgtXvj(F#^hJzv0OHluTChnwwL z54NO@t{tPQqo(9tVW?-kY3jc!G=|;>7&(&bAJVyBaP0o-$|IkKx+ml`jlH&YW<&b9 z>W|rWOp?2yH1rH;Olo;3zq9U6l8MLWdGA`63*QSQZ7=AJL0}ew#p}Wymo~1s3C)yl z?8p2h&t;UdHeB7jMFCZ;ADc7AVIK3cPhoKk40Up3Z?|y*dVA-vd81kO8$+h`S=PC9 z!M8+>XOTOVyv4ow;FB8_m)6qJq#Yx-#5Wb~%cuWAm*!f?v1$7$3vVB~T5Ftk+VXM3<1%~akv9Vm?Uw5yKAnq3u_qtdqwf*4 zW{Ak<4vsl+WA1wNzEIJc{&R2dxbrqoaDH0u{O5@Kk1mwVX|Y{3Y}AJMsL2!9gqu;(K#eos4O^6kNH_2Nc z&m4A{^|XJ1Km0AZG@Sy6yI$(YzU$k({p!>7%qRiB?*7)kFFpqaT^iS)h+jRisFVI^ z)$HZx4u=<7u0Pqbr#bfcZr)}uIB%6t_ctT#nr?5;ba}%LviPmEq`G;#J=Yb-oINo+ zORHM&GU}R1eS%LxbYWy2IL>9XSW>RCVa7t?aaOVAkE^w zx#!h2FM`F63fmo=bMOYP+MM6>X2k$j;Li_>k7^&@ezN@MMXl%YpRVR&#*Fp+i|WfI z=k{;Ivih?2JWJWfh*^3d+BI+d0`Kv|z$L5UMT?HE7=Pp))+ax+yQsc$c4gfq^0jr6 zwB2$P88VxBE z9-RuDoAKsa|1Y{J6?@sN<-__f4n0a8M`+s5%EhH6c5c|TE9s4xv25#l1iqJ)oBihJ zHS}4ovKLn-KYF-&?)`VV%k=HaXSoN_+4O^3T>JNCh9!k2cWJr=DI*ss4~5*~NcR?e zvwYd`VC&jTHK+2gZkdtL`t=6o#o^xfGCyc-E!;iQt?Cp@9Qn-jdgA0Oj3LW2UR;Zr zUU{it+4OeDQR3HFP@7QM;wY$1oLQT2HOa%`t&{qTc;ebBpEdxvg%Ln}-@I@`+=F#N zwm9&?B3{3{gHldDXXuJ?^wN8Wm#6PmyazY=cw@b<*3ov%-OP$PdE4IS@w2Iu9ZOey z?d>$aymq<mz!smpwk!2>S7Zcfx19Rge(7fvecE5bYpwu@PvT6 z70mmOpY0(u?V3stgUp<2S~@dO(gt|3N7Il+SA^yV%cU1}Z+i1?EI===90yScKiaBa zxU1;PDxYhtByqsg8eoc~m`*#ewN^5k92624T^o8TFClr^`}ba&ho}AG3NF?{h%|e{ z)^UQ(6;t!-a_FEqcV%8i)A@C$@^f8huD+l7THP2h;ari{ax)~iF-c0!JQTTA@QI~K z8S%WhE>{<@PUOd#n3ky>=Ds$EIwWi2RocFbTgiUSt#_7;>?NHUv~J3TJ7V7j!6&vJ zf4<14TH-gqD$|fqdhC%jv-gR~o0@_e`PM$!GTy#%A!6DhF?i%4V{#_d95^M<%-9bk?C03sb0nzg|`}3eUYsv-BL(14LzS; zENl);{*{`K_N#2lt_Opj-T8H;ie%$cW-&=Im%kItoNIt<-;njkKeUsKXE-#dm#rSh0om? z5gY6Oyzu?e?w6`WX+s|Hr%VZ-1fOsRnVT1k&@8C;#5Foih}knrF(jg;GoUu1bydF! zVW-y3<>qW@oYW=Fx2SKVb`Zahw&zXGpjR=y>+e|XUp7(0hzr%cyNgEjuh`nJg1oFD z3X$0c=)3Sn>4y?E$w`dMZw?vuXXd|8r^{yczcgUNeBWM_F-1csJfd(8_U;7}y}k5Q zG$ye>*bTTeuGjSX){s85lk9VkhunR99Jltgn6OoU0e!Oe(9lfQX5 z?5DEz1A0UiFAmtir#E@`)=m)tlFCjc46y_5S9ER0&1RRuHpv%`Tx;9$c*nY{@2;)s zi>wQ!Iv+jn=Uw>#djid!XkikAmtl5K8_>&Ves!kzszd&eqyFD>nmovT00%&g z6^FNf(I(c9RQ~+9(QEOVYeCt5w&J1Fn?od#rb?ejOH65*QU_ z%Q~=X2rjGTi_ggmpQz7yftBy?$Q!p0x>9n0*!1w#jlil3q1S@qnEqpK70F>+k)e0Y z=X*_khJbYKLpm+)How-#cI57@hRNfjCvLoI#snDGt(AN}xq$6`rv^Q75HWMX`fV|j z2VWLm??sHJ`Q`5H9qcu|vd*WeTUmhM?D-qll#PIQ$@RAEBU3O(A`Z`>; zq&7fWsMCMd+%LWM-u-&EtL11Ir2o}>gO2OddO;{X%7@R4d-QCL%Ch;zNq9V9ENAWh z;&nrh_;pgdlT<|Q*Zz3^0XJz)@UpMmGbQ5& z)aSxa^~>jsP#+1N317y120Q+Sk+Akc4}~TqA9YUZyMG9O2d<#Ytbp^O-M3EZt7x2Y zQ=B?b)dt92doI{?e@o}>F`AmnXSLpy70LD8&WFwhu1N)XV*2~K@48ZJIX+|ew?miU z6mAw+hV6cH1AfH6(*y%;U-g}RfN4YBQ^xe0yTCgFhff^A zInyLMP)-G+Ybstv@+uK3VORIw-P!Ees}bRN)DEeWU_VVM5- zaQEYHiK%<~kT~bS5hfR*|L7B@eg6TuCp3o(fN_gIPNjHSDECFS~&DF z7TT+{Wv|aPz{nl8`MFsh=e+qj>@sP5U2n}H?ZJVa>+;-#4W}lo-g`vx#CmYoz0(uA z47>z7@}=Y`_Tsu(A1B^s`P)~GBWjBPqbFmB@UFa9*1ku4inyP1ul7p+jT5#E7>qp1 zOZn=R;UCvYFnhz2Ye&cQIoMab5pnE*sXr1e6cwMpKKOWcm$gF+`U3zVpax3$$_JD4 zGk=oYE0NF z@qB?qIWq`8Js|!{DbVlAiaWkl_Y!XHDXiy~wfx*XS(E$9IjOm#6nMN+Ty*Rf#NWo~ zv5)4reNTmEW2iz|wtqyBaM|i@8=H6Q8pD&%pJmTqD_k}+b6n5V9iwC7K*vK7g_er_TL(q$H`u4pI@qH{8BCHGkIu<^FH551ctW{W+&nO3vp#Vldyp zW&}(c%2`pw%QkOK75>Bp^k3tfm3ZpOs0CNMwL<4-cD?xoJbp`KQ2m{UbDoXcw>@nJ z{sH>*gXt$0bT-B3EN48LG;fl6Q~Kg)!rn1x1ZUZpl_kBCX8#<$rgPtOncAPbnnh=V zFW+cpVXiG$xopc%r(Tb3y(U#kBng`G% zcq8UZ67$Yx+R6e(qm%Ajnw3g>gDmSS+M007f9<;G)b*A3d*8oKYb}4t7}RUMTHPE_ zP!SPb|29;E^NH)*ZOzOJtzXBjW;V}DY6EO~G;{u`_rJfy^uM}6bo26>HnCSr!=e4H z!$!7{?tcjL`!w@@&D;nle@{Y>OKDTqa>Kle-|W$emkfLyc;uxa$%k?;sl2!!Eh9Ay z{cUH?elPFA4+kzH_0IGb_s#QHemV7VfMM>GP>$grOyRM@ht{Dji3qvl@mL`hc8Ufu?1 zY6GNCEqbujOH&&X_f-DkSo1tPsp({SdBCXhfU?MeXU|W)0&1MLyA4p7db$sIYQn`q zmPD?pPpWw0f(N0mA9U`yc3SJyqX?Tr>eu>&k~reEdP>s|<6yqzjKJF~dE^8vn;l{9@}FW5pW12dL3=<1r`*h^DWASYgvHiWyYD2__rZLF zPY#rSDtbO&I)B0Vg{P=nMN}W^&{Gv<)sd*n<+0}l_|Pt|$Mv6|oyhU`yBJz6s{9af z3_PXNhltp-D#I7#`8L4mt~H)4I^v1TY#k%6956T5=UV5)oVm-VH_Q;%fIM-dl*va5 zD>tX7v;pFU#6(Hf{p5t!5lcIl{ZTH1)Rf_qP&#$$2cBJXI) z_mh3joY`4dR#Kg^{b{#b5hp4W6Ma3gR{Flj!*4ZZr34=u`9+#@>uJ@OL&sX5POr(G zY{@(RV0x=zY@;m|*t*J@%`0gG910DbIx_Cu-8O)IkMJ_)siMk`_tnAuD_(!4o;gom zqUv+~-76;fUgv@R@eWcNmc5A@g zi4WuaFL1D9ys8eTQZnOZg_nsy~ur zmg?#)3BVwryG*%pqx|5O&6`DAQif!oD=IoCUbJUg<^0h>@aIWGdW}D##d>l?wO+}~ zZv-AI&xp5mU3cZxnzTK0HVgZ1x>z}Vk4Y%w5g(>KiwI7N65h>F%}G9!6B5h{KFB~o z_gtP=ykwMjV7dL*TeE*mT}cuj?vJ8nW%VABGHhO6hC2l(sIH#f$2$svsg~x3?(Vad zNxMV77QG{GdDi_${OW1_4lG$ey7K&pWg_om&8&p*l_$OlVosMA&B8^UJ(0`Z-S5c6 zq*FT#Y4bPSytDDvt`vLuPr0WOY%5L?_NlI;?=D!fQeRa%;632FuejIx_qT#hFMqym ze?m@^{Xx>jB1&l{hnPcrw)}cUPAs6Gbawp7c|N-h#ofCHKM$Ed;SM1$|IMwf=Jf}s ziJ$Z#oRvN=p+x1cJszH0RZQzo`C9bmOL2C~q1*lx{Rz>)tP)pKC}#Pc@WBmlkv_FG zN#G+}-re8Zd_f2L=Q~Ev)|Q)RAg{I&jt`w&Ga-OG;mur53hIIF1veXAJ#yUb*WD5? zRcyHH@;Bd%kS%|Cu>W9U-AjP-aO`CYG`E(WHSN>NjCWfCmgMY27AOOqSv8`~s%x+% z#)Jte8z+`N4UT{D6btP=BYW|-FY&YP&bxQ2XY!^6QPW1!H|~jj)bs}G(b7Z6 zi{z}?=S}OU9&iuo9+Nuo)AZPrY@Y)e*pr9-P&W?=PtWxcJrjzkYohKV%X?L<_Z&V} z&OaGV7Kez+k04LB+z6N&+&b;Flu`cEK16@&p>4rQ5yM2JVas}C^>4umWg(XG>9GrU zPUJoL7<=vEhS6BY63xm}?lDjLUIrU5FVx;?*lB@-d^5>%xcWrf@WOexxB$GPe6ty zQJsu9&g(&guvNc*Clm#>m0;ul>INe162r`hsGnGsc2JIg@*_kRGc zM`~-6HN~j$AzRa>=5vdHy)MRx)Ebc!-d*OYr&X z?u)V!MX@&c;!ef)Nfm0rDAwEeXXdQR4IZ5v*ll0kc$w_XoAn2yMu(g80Q3AyKOjR- zzvTf-cWuAB4x-(6;-ZVqtn4y!bdp}aYHuMEzu~<%JtFC2p#1+M?yG|02)H##2tktI z5Fog_ySux)`{3>bf@^Shm%#^jcMSs!?he77?EG7GANSsU*s87Sx9;cD=X@IaQ|~^; z93XlJeyASeJ8ZfaxP$PA+qf8J8QgCyKX)CZ#WF;bOM#s&fUM$5#Z#tKa~Y49UEV|l zo;|o5YwGTf+~}PBo+lWD+-tw?B zhQ>CRMgS_6RRY>$T;j>^#*E;q1U+zaa(8j(TUiO$FRm<_THX&2-;U55ZgrXTDkh+l zNA{YWXwdNY{%q(ah%ns%LGiz7|C%M&uC?8jv#U+UONO1~5>k5rWT6D0JKPBPkJ^_^ zCr{e_!MCfH2sGBcX+9Ee5l#SI0?x5YXtbfKSP&Fqt`?u1oD>%aF!a6pTt9ak=Ks(( z9~+|_$zoia4){2%@?z(#og6ad97{tkM!hwH2Ni{%!Q%4in zWt?XG985`?>t!u4~~F$nfZ&zu(T@(n-N1 zV#GP9Vhcm!W3Age2Rl+0AK?2oOeo9`^jy+HGZC0YU<4zL3>9`x|=mEwWW7K#Z*rJV7g^?lis(;0uQ*f zj2&M7pDaK~r`I|GkmMmLrCw`qSU=jE)9L)uC-j>7b-W+NHngC7pYRpTlQ~MpzoKE0 ze0xawL8EJ#sPr(iD!6YducJbq=t-xKXu~qIIqS6OS@)cdPQs7W3UlxZ@I>QZgMVQk>8i5e z)@6BwGo$uRG&AuI0ott0>kSx@Cs{`Wb=)~3+UiPa#H~x0D44j&ry6$ZcUU-`M;5-J zu?&ZEVLyZh`o*$X+3iz=A=SQyw)J}d>M>w|B&@XGTuB*@K~cC+eO-{M&YsAVe_Bxi zdMoItxgH3Xj%*=zpt*N>p!Q|GbtqJO_*Q@*_at_zw`aH&#O9i^;C=1OU@_Lk!C9J* zHo;Px-^bTRYtmILq~bV3K^Y$@Rfl7(dxlW$x}0v(#T1AaYY}0KqIU6wy{)EfxR^Q zxdaV`SVMj4hYI!&xhCt4#)Ieke*#VC5_CUX9}2G!u<3PaXpwD-Y9-mur+Y@Rt;y4o zJF1tUzYf`Jk=Bu&fLST6+#S_}mf^|V)EboN>-HWes+nq4I;T^0am$#&fRF9z9V_-SPh*9je zcmEn}`s_@}S?YozN9s&i+6!}PCr0Xniv{wx`zL%?`l zmt_-Is_0{op*MwTM%e*}-ac=sSX5yg1vp&AP3^<_bIoHQPt?$i@77+FO1;eYc^q^Q zB=ljr_NSk(K&W!^-f-zh+lr-_rG@o4bo=bAo8XhW2JatBPqq0m&rK1(G!#SpZ z2^X2*j*0TK-sNdl?88?=$Cb>B#6=zco87LSx7r8d<@h4f`auzWMBx<$Xhge&> zSf22-i~AWoxe(dKq#m>+$?1Ty(DPF~J~@c(PE>l~Kf2rTu~hd>{EIw)>zG~9{TGP8B(FCfFfuR5~uIq?d`gy}(BPJjIHl=eIV*HU9s zX8n0|hiKlL+IZkIP4#zY!8spC^qXB1?0$QP77<%jlV1({dJHD%Z~rDFwL;jFH|H!C zPj7xpxb`dfyU4|)a2FoQaZgt!+NHL%FCJb6nEqK$8xg|8-tW%5w?eS0#FK%c{~UDB zpiN*tRs|kI+CR6C`a>>Wd>d*xG%g@`;rGzS-JXH3?Ja8~7?b6Bg1ufLJVhb* z2&ZjQUI?&I5FPo^A>vNHfD|*@nwXt8kwihzeJI!uQ*`NQ4UdW_gXb(k~E zReu>m8V6Vs*%_tf};VPW9hX~g0FOQW8Prq&Xo zz?HC|P9K`uTeHtPOw8YuL&SorCRD)lBWem!SS<{DSFtXzg8Rz=7mpf;tNqG+7wm3F z-oA3VdTPI5d5tM(NNmb2|Lh0jazmDJYSpVKZP148=!|-P)Af+fb+;MbaRpJ9k#L z3*uB_bP@96#U??nHX71i7Ef@njC8vwkqMBA?PGFojzkRPtqF08P2nF5<4@`S{(TqD zBR5?+M$eUYo2WW|>9u=dc9UlZGY8d!7W1^Oa51j)#OT#p1D{J?{93Y>?Z>VerjDii zEQ4jkN*(tB4n~y#hR{NoMY#Czx!b8W@G(FB;4p}Y3X1%re>R&pNx_WyX8u_-C&b!EOubPedZ1pLf18Fd@GNO*L$5^;JpCMMq{@tv9-WQ7S4$8F~J^_n$fh~jA#Caoe`zmbaWiqBrp9`u;nlZ z@vu~)y)T`CCz`Np+^M-vr_tAL+@48n1tdDplh)^ET${bQM@=33zD>6QDLb(DBP-Xn z(c)3p?9r~tNUp;sCyZgM$p68lzE(9vLUy-dUl&~@v^%!K5E&DaE+R`2V2>={%APac zgyg^QDCh7F9u{kg&?Z)r|2a{kriL$5&i;!~6v3Y)N;*xxGgKEr3hsw0=Qg^SIn+*# zAXvM4`DawCl6lGN_zA?nd2wzVDAaf)a&5&GE4AK5Y>#dUwTLvYu%2R=eJFmI%}XZp z-ovr7$}vftv3h=d2Md1M(VJ&kK(ELdSoFlC86Ep1J^_;F9={f1`p_rP;E{_ug-hYu zG!}Di_lMTNAX|-G>r7MCiH?G2AD+WCU`dEb3*MdP;`SykqRLVcyvuO%qN{OjdmPsa z&AaU697`5#`Y_($oy=U8EcYYi=uAeD40l4K99>N%#2C>eP|IKJ+YztfMghI4KIT%3 zzmZV6l?_BI7Pu(RIUdC)M!wg~lMMUK!HFZ`=x?HzSUtMstpLCm{JCA|EbP(#A56Xf z2dLS0ChyMh^ML-1crQ$jxI#j#hn{2bE^&! z0Xc?S1U$@CTl(3&w_y7Ht<{j(FI1lJoM+LBy4WFubqLud%PQMwl^7)nUV;>Dq;1l4 zM&;Pup4dyb+LXgAeFf%klj+8^w!n)AYB_se1C-T!r1 zz`srOcMgG*Fj>^b-6fksCSq>ZEsQ`wkTX8hi}S0t6Z<@a?UMR!;&R*8MMNIS{h^5b zw>t8<@Y2l?@WU|9s+^lF(E2wQBROqi8&y2^R;*$Z`(euY=iu+PK{bKDnV=TEU7;>J zT&$U9y)PLTIaeXz)Nq5qGNjSG zM_(Y5KPnKC=&2helXsEmjSt$$jhPm9ue_Joa%)?%#o!@@$&g`AxqF_*iwj0owG0|R-0W=oE=0LJr z3t{R?bB7^WILPEuq`EpmBrb3gL821kXCUbmOr1R(ouQv5-?W)#Rl$NimuFK{}_ zy5?<^qTXLKTMvz5IOu3@}2W$s4Lur84sa}dBr!T z_51Hvza=`_4Y6X8U)+D}x21}Xaru^#I)EqNO|)x!+aG*J#L;?Ku=le!EFmR9F`z^hXM z0~SZkwJWKt4yc&dFSf)9G+u3uO9QQTqxDc1A!mweg<2cQY%@Uw zLs8sFmf{1vVZqX7_>$i9vm^gB)wa>^79ObHRC}*Arv~DGNYHpVM)X95Oslj@v>!hs z1)TDGzY1IW)^nt<#MQYz=p!k64mF-FytU5ckq2PBl;HNgYsGV-|7^Mh{K(hy`G%w*NQg^D}UnUzA^=!4MtUODMsTZx_>Qm5J z-SaA_Tf2q zEHofBLXXHFF?uRccYekw$>8~-N9sB6imJ^|CV;DXzal{3mGCMA|9W3*x%rTJ2`#E0 zi#&HU%h%{vY1NA9lx!}3!l1D~KvmAtg#%F~)ue=NX!jdZ8c@jRo=R4DlJFMw(u*jb zob_d%gJf^`XxC8;HgPXCd6CFk_KB3pVw#sGiZ-~#+o?RGC|2E9J@LV+)&i^}L6yue zs;7`hqNwUJrXG?)i)l@>zJt@ykss*lBwga8S_QnwV1VRffS4=1Gif2jiY{9rHLt1Duk|lP}6!@$6 z4@N{AoW`qv2d5^h=J&u^TKm+LQnOuGa0Mw{OO}z)qB4(f>CYvHXai>?v)RXFIb=+t zeK5k>k=D(wi@S1j^8{<<_QT`&NGpTOJe+~}yrNBq&mBqX%k)^qk0=RP3KvS>5@^2o zs%659!($K&>qyWkIt8fq``oC*Cn7a-xhdB76hTt9M{+#R-pT_`s1SshLm~xW^GO16d90n+#bUH7*1x>xy+>Us6 zK7eSP{um%ixP!35%5~WOxv1Vaqh?5G;L(sF>TUYR>OFwUK9LEXt^5 zakCc>Uf}Ia)u2<#)>rc=Ul>LA_h2+8Hn}a* z#?ZwBK;+wVLruL!fb6T%igUSo!gWu@KbVEur!*EjTlE?NxHnIc5p9y5inVci8C@Z@ zE=m?!rgV{)Uww7(OFq2`Q+^d9s_1PqVqo^7g{i*BjL8V;B3G#)R+^5Eu`h)ByD{JH zzXdPqllY{zo}SM~I-di_X1Ik&LyD~>Vdz{-_QWpe)P9-iA5Xnez}4Fbu9x!~&bQVW zYP9kU3rH{45qH#dkbS5Jw(?V?&ZHQkrHO6(I)v0Dk?EC*rP3*5R=_w1*}VzKlO4U= z9yvg(c(4y%4xrkE&}rC}=Z~K}V&LA9ON^V7h1K#)DE^9Vj&$K35p8d@90y5aXV(_A z%MG(B#e8rTk`nV}9V)Gg6KOXz$25pooJ!ec{ZyY0x9{6>NBWqdf5rMU0~X-v5l3Pi z;F`c)t{h94CCe{q_j?-br_vpJEydfy>zD_eSWV02LC&rACREeE=^9x6+IdM8J$gp@ z9$AxJ$?NjLab_n`t|aG#y458Kmpx^!B28hW^^47YHVIZCzvlwoqk|dCFIAV(_9(T9 zLKKoUhEdkWtnKRqWL==$G9a$J7|oM|dc`(e4v(&Fx7w1rS|%b6m(s41)?T@p$B$Z5W!?;QgMStp~n(%cGQ;u0mb`D_&VsVLd%k+N(1 zFfIh$o%fj!n8`&a+GF+nL0@&)GJvsFU#V~AqmKgCEsXSYzWEn6{(cQPo5H>RN`4tD zt=0|q373q8N&@%Jf6>K6xqNbF(O-1%wVw-( zXC2<*#i?iL`{lkUoVC}?nK)m$gOw}i?X79HPi+80Jz64=&rilt#qDhYgNIe0)iA=S zmW@giE62vIH;x}A&++#_xJdmOyeS{(6T(QZ@iQJq!}-P6Y!tmuD!vA$wpXS4*bMV= z)i4+eXB%BnQq+nfeW7g!`7LeVzK6XxW~zB&+`2SeeZ$_WsS54`Ac7Jc=g_IR0~uEmY-E~B ze1^!HE%sVwY(}&vLw~TzWhKd+BoZ`bk;zUg;yAXpT|b>>@=fK*FC`mH$~wgEIe6@M zt5xlRin}q~^%T^LI`7>%>NMvY?maq03FU)L>|=ompmR{co{V-ft#Sxlg(CeI-OH%% zp***T$TogjEb`uF2ibKypjBoNHByR{U2yG zReUyEeZc*RzN&0<>r_Ksd1#&G-R})A=M+2tX_9~nHG3o;$Yov@?E0jlQ?$Vw~n+mnlxV!c4*ENfl4!@LXY1Y1WznEk!MkfvuMj`%Ww-s%UV-qlUQREkNH8JL& zF|`kI?sRxa-KHXciydCTx|zCqS-LPA!XAsd!jsrpjJgxQ)m@yoyy&#JE*tE6S$*{E zY+7yjEs>SBTLd$h?mAGLr`PfD?WC6@xU)nmX zTFFKY&RcSF?YJEkN;jVUFRDSAMx3&PGmwZOz-|qnP2z&D?qacKm$Tmf!(`(7-W1^8wHDn=te%vn0~t>r?Y`dxnmx`;1|-CB)&;0t^>}SR{(~6} z5IT4i+=6OKH_xBok1v+ZzkY__Gnwrr{$dIagIdpFhPCesp!2r>Kg27O!W$>Ssn6z; zUhjz$4!&${=+NmX84Js!8%N650N!btQ*8e%GiMUwpSfFmC1yp1yImbDvO`w6K;592 z-6`hn7*pi+|Avtb8gA!rY(SSyE7JsroM54c@?1nG=9Ax5`a=|Bk~A`vzo`fG^_ZA! z3^OS}l^^)2@F>rrzvOp+pI_zug-bOV77S4MzTK-r8ObvoJ9GF#oT{<~J)h%e$-vP3 zpe|+}@)L||MbW14#0uI%S>+5_S79G_#2FX`@@r&jQTiQ4dZ^!?;EWU*nnAMwyKCcZ z6)38gg;51qu<2+1)XH8XyAv{(XdJ~&Q6*_J_*DXrAVhbzr7l_z{N4(itFzEnY@vJt z5t|62q86Ta*(>1zDrSkI|C%SpEl@8`{77(S3iAo|RPS!1js*|*yw_299MHl`ufR?w zpoX6=)voN}CZXyrkhn|cZJ+R`umXu3!xBxRs9crz{7EF(UKgv!!LZ4O><&48NWkjD zMSy9QPfd+=0*f%iV1xTr*oraUMBqn+KM*+t^pc@Q?hekMjIu?PG*lAhY%j1u7aO4$ z$iZsH&gHo}Ybo3X4(|4bp!}VM4l?mJ+7boZZwO&3$TXwumWABCu)YWivl3lUP_R3l zD5W+ASf!_@Z-}?LMi|4L>h?t+okI7_)*lN5$63O^Gh=9i$NvLI7U7=A%imwXJt)Fd z?KJ1$zUA~u;*%R^a_9&k4X-Lcrio-`-+&d7mCFL-aRp0H#i$xE3zkhJCzdmiYRDCD zG?gWQNw5=1OYCN?adNU2{|l~PEZTmpGS&JNh5tivPw@61%x`g-0oCY>54FmEK`1>& z20mK>IfObZh!k=QRRB?Uq)5^12Z=Fdbe~1Rb1F%h$|>s4l|sgwb#<3!-%H;(I#CL2 z_Y@-jwDQo)ZUo^{Qfs%_1}BoqX6K(t=9b9p(kCL)^WLF`Fu?1+=WGQe(#dwwT8yff z>4w_6XcTGx-uY9anIdDt!<#&gh&m>~vllN0)7KRu0`(1F>h(TD6_yB&e+*0yRKz!}{%P1&S;EB%F6C9% z%JjCr*2-9jn8{4}Kbi8OmcpRtpF7_WP>6faKEse94*;|JZaRfruDrav;hm%1hpcKf z`O^<|AwsXv^eqNGacB+`u#l1xFTT~+Gy7c7Ovv$pa*1;$W7QClgspoEX?%Rgt{wq8 z>SP)=VgpMX(a$qZ`PKgSEiM#XB9lO=46+SICzBQ#PpMLOwppN%&#uSw35Cxj#+3P# z{4K%^`xZ4^Z{q<75MCmYXUI7ugUHLCD zIPz!^1lf5w%1<=w3M-*y*bn!s8U-(bd!Df!Nj)weR0M5#ZN*xjPau-3u6a7qCPOxN zOV0s|2c#M2i~6oYyV(tVZQXRDw%Zr$;#nBPQuRn8c#D%&I6^%Rf{{TNdAh@UV+>jhnzCBvC&K4N z=s!Rt(e3j;_J(Tn3ug&^uATJk;-2gA^R|*19~(jgptY!z0ix3uIVR9eEagHKuHI)} z@HV9aq?$~sL7K|j)clO93!+Q-G<>$&uo`EuhC7Xkna+|0o9{z?ERnn0OuhG$;Wd8d zN(yoR)udw2T7SY0vv1o^6FXlN&;&l6}tRroC?EB&PR^KZg?;7%M4ZgJ^w^c7Yo8+uc6r< z{bVoc5-;NsXfhII8&l4z_YGk620H~R^v3AW>u3@gcQh+b?D`-}K*?^iJ)F#fCsUeI zH-n%FF^HU5I|XQH#Y`k=10l^=jxumYVzjMidjeAoapkS8dMna$Io3s&4i>C#g>6>0DWkmpO{*L=PMa|%-5{F zvO;@WB*ZCsck#%YcK74B$s%uC8FxNb4(YL?0&o!P13chEeWs*tL}Hs+aBHFU0z$5| z=DgTh5bk&)J$+X(KZ}K$*{wc&=ULom|6`eOi8}r(+%5*(jx)%8oETs>74=+s@tcZH zrY#|;#5Tju!7F9fF=4Q>h#Ft4wWGkQQdcA&Vm!R|fOpNF)TVbDDV4DE>=IX$4(@Q* zb85YhE7wDtEOl0(D?65?DV71O&NY8bX55n}8doI!JVpqsnG6UlPx$)bPTOHUVfHP4 z{>~qFuldJlLc6Agp+>!r+lBYxOS>LhfYV`n=54wxMi%dHwyA|sRyzL=rZR;n#D&5b zKP{UqdtutNsKi;kt{=*lNJ1NUCoAUm*NtW7f<$LSh8&P(dn0+>o(Yr6a7(Qdf1(%} z8N=2Jp8~r!>|4)s9mKq?OBX)VnRk^zno6KU>mLl#;6zK^@q;@*WL{Yf9f8QJt*z@V9Ba)ZMr#gS8rjL&NvF2k12)-SS71xi zq=g3t?Z~ckK``Ae*{DFQ<8+Jg2IG({k-koL4&76Ig(|79Zb2yHQrj>Qv>P>pEd72| z@u7of*@f3U?%C(m znrb}ZXhvtoC{gnBlU->_&cP|^MVxnZ$-g4fA7$M?mCd>_mxogDE&B-}?dpVN_Pdqx z+&mbvdu>-`YbaU}0Ut(>?IeExQ65aCO#7(L4y;=hfC#3j06yEAe+uG}g!%Ndp?~D1 zEbZ}ELWkIJGWyzKC75MrYD+*iO+6aRt<*s3OFQ`$bw@Tfz+rY$!G(QcUj5JP!w%ke zT5}iC;kpu}${l!9Yb~y z_HvG36DtCZu&|RT)cnbu9-DArG$xMhi*J=SK#}`{MiQzR6AlUIlmdq~gD!NNmpJc8 z@Z7Ece80tk2ZrdrXMg>+)sgrbnE#had-eMpoQN%kz3O~MkCD9PUrQ_nFu7u6M>W(V za=)VLLl#aUrfbxQ`F6M9E8l3^!Z`*mP%TGHq`yd zwe>T0Gj>M|%zWGK+?}>wuNT$8uDKa<8b}J z&EK8kI4O+Of-GwYA!xJ#dI{a8-&SAP;pS zpH2q|a8I;!Mm$Y$RatY}Z}SU>^d!a-kx-I zKN-q=as+1E_+EjUx&69A*+seA{II%G`kMA=6Ojc;GiMNkiF!qW74#&w++UHA<<%n| zcduCB?5Qa%8{59)>AxnZ-iyZxu-d(E}m zxDe4z?^nIuY0|6ZA!WG{!;?T!WS_|dsc-!w* zwI_lXMx&U8br#leHYeXF+^?JOy2sb6Cy=ijysqHU(xhn6ES+_JV{`m`b&98_Bs~^q zlSnqtgjUVXU$PxY^zHqGm7=Vj6@b9>!1eWbmw`t+T@T@kbdb~dk(R}*ZMX-@in zCF=bvzxvZ?W(_%V@?^Ql2ifm+8c7S=cZN$49EnE@mP{PNa*m|t$tWig)@Aa%CL+Ay^`TbjrIh139;An9ZTot=4~<-^9Q1i$b}-;UyMJL{>G%#)U%vo;0=yc`|#EH zD#7&b3s=dNzI3;9!>+{+Q$oev|yYKYXftHwVq{JtchE+^34_ zkW3q@Tc2SWmueQsO&h6wZec4W%$p%-(0gM|>K1(TJaBjzlV3|6=Vm0`c=Cims~(oh z0C*R?JqTm+&?pFf8(Nf;S`w`?)pIr2sQE3dpVJRX1`-QZ}#ks*4s%jGO{3&HZ<-uTT;jwjzlq;lwS(o; z=Xd-T&KRJ@DP2SFRmC|5m}%0aJFQ*PzF*LpC)0d6@HV6h*=fOV5y(Sc4eN_qno8rS z(swJCs@||aE?ba!$e1U!baMcXC%jBEN%Sp0(+aPUHO}TguB8#9F48Hk9uYKgO6S}g zNBovqP4vM8Ig=n-WfGm1ql#BZEO=GLU*@YQL6W;aFg_ z)w66ZQJGG31LJ4yZ~z*{7>!znU=|F2kv zo?@VzyWJo4Mho$7X1H3p)oB3o+Nged5qe-UOeYe5jBur4_36b2^FqGDy=$^>dsz-9 z3myWMFV^ZF8X8hkCV#rZ)iqP-I$Lt9GKbNax~RYsb<11XsyD?8x05VdX32EKp;GU{ zf|oSlp13cSg-4*jQr2`!DVDS=&w2Zj)MBd3WeMqB59q&Tqw;@iF*hbK-|B0A{8A$d zce0KknaRF2SiB#lPPkr|k)LJTmelzVW}cZqiJ+xToUJUtqT;At>ReWGCc^>5QXpFa zoJJ5^L%ys*!w*|-TrAPtG8yd}CW~uCC4iS6Wc&5weRGcsiRpW?b`3oD$^I$1mhRDx zNBLx-5V7cV&2|j_#mA4GwGK zCj0l`BN~-Te2d|M9OEmfXi!eltGXGSjKFTD@88-)Ee}np#8HHC%lko4G5p4}sP-cV za&MpgASKyj5Wrr4^~@V6^jyY0x?31y~i~`1`x{3Z1B{5gU(W84`N1`SUU=zi7o$Rt~1h zr@nKf5ZgJ{3T8fKSv3t55v;a=z{$n5-&u-kyWOHoLR{h^#8-0E8sevn8JL|CXvCWRwK<*DUjp3duYifnhV|!4<-0^Z zXl$&p0#Z_2eg;$+XcKa3C(wLM?nfAw8hb#LWJq;_F%~CKE&I(6SP$Slxc>V#(mWxr zF~$3*tZJ&x?-<+KS zVrZSnBB-0H&=XgvTfmmj+*>GhWC!w=GR>jB1H3yxWcAmNRV@!*T%Gs-U<7g3hv~E^ zJeED40J(IDcNtNK-FDhdL$w;tL+1hM&4OJCE+dPoHF7P(gXl9yxKb*MT_}ID{k@iF zxz8P)x{e(O1?t=lz67;Bf0$^@(#xzJznCyRl&Yetvcfn->i<(E zf7E8?8;!winw+Q^3McWCU6yEw0xfr+SN5YQ4pZvWQ{!k#&Y#Zae=r*c-&Xvp200(7 zH#+6^7+Gk5(DR^aMvcUxF18*JF(b{qG=4H2jj8b4{;-W&AZVqFm`FH9S?~xkhMHkK z@k$A%gpWAxd`azIqHGp9A0?5k5vda8 zaBmX@DSuWnY|ABAC}ZK7Hp+0v8?#G$0wY8WV!M%^_Iu*!9X_y?@|jE|QYn6uWKsVD zM5-%=BO>_ zkbfOly$6G6u`qXIl`OjE&l9v8c>rcryNT99*QIgQ>&q6i7HG+vVa&4ej&il0T_hcI zzVyt(owQmPjotbnDuKB@&Z#DDuz1iyo)ue%Vx(?65&l=>BfregHT)A()yg(M6hit* zW&Hh|Y}7clF+;3d`h`&(2B>--^J);9n6e1tP86s2nsW=&80vYEOAjVkTVlYR0_o2g zw74nR*^!HUriy?scz!`2T{a90le^HNw)z;e5|)!0 zl%_o02>a4%QU{4>Z9RGnbR;1d*;|SSzdYO`TQl{FGM7@Sz%-j{*7MVMgaco+0`QO1 zla_cs_8v2i9zm*pAAEIvw?3?To~$uXK38E6EmjV0NJLi`%ne?_{!${jQDiPLnKrs7 z6hY7XcvtTeZNh1zs8m%h=UHCU${VS&K-gYasA*8CcG|#kHnykS`h~-38X=SXTpGpN z9@9T20bgZ=4(HFA*la(+5gtOpmn$hPbCIK0B0D4R=7Yfs3^IV79Y8vV+f7#at7S}YI#dAp z8LyKXsi?5vpdT2KN%OBsjGRcuOz%55~Bgte*g83&-&O z+jiIGGC{?jBB7Vz8AQz$beF03?6M6#2LkXDp6gfVIp%%r(H6Ww?qQz0n-mXa(QTuL zFHF8kHl4RI$?w%X#~8J%Gf{z(vOd$9BN~)GP@qLlveSTK2m1(^%otdt;jkYHFIf&N zcpqCcVna@f+6@Z{WBoyaM_=m@4DjkLzIy3G`wj${)(VmPiC-$Nj`{zCF*AB^>>qA~ zjzpeu(WD&&E#ed(&wt(r_jOJ}SV;ZKS!EjS3X)7eZk$&mvIw# z3yk>56LD9M;M*+Cxu?O2UE!UrKh-JcfK<*JMJDer$}~K94e32f^!3^?& zZ^9j0*X&(}{cGfQXN&-<3wlz2p%o|ZivH6}_FV(^%{PSn9Z-Z;4h*eTn{JMbe0Q2U z^H*6=sl!ww5?N;$ivZuMlG?5O&I&p)jC|(8`)f*YDG{y1gY;@bMv)%!$<_2Kl%7rFLj28y~>P3miG9}eomxrJmiEe)%d%=w+?-O zczA=h$#q{SOw{?*F9T6qifX2k&e=aQFr+a$e#qzM{SaEd;9F_bMlZ5d)->Y*0$wf{ z85~-<9-z=(9M5I7K6S?%$zHN8Xyvit%iiN!VB2Sutp`!Nwa$ z4xZ{06VxpQxlfOXh_$rzFC;rS>&N1~7>NGW2Q)x!_KVL*M|vmQg0rye2^jo5PhL+knWBSDbw zacHmRc}xNP;>a%qXHV(saB8~;vO0}+JWjn~DF!>@)I$W!sOGR=i>@yRH+YAVY(^Z4 zaxQpJ(OOmvTSoMx7C2A;S9|Xi9BCM~>&6pLY)ow1ww-ir+n(6T#J16~J+aM>Cbl)< z?EJg-$-h_Cs#OdB(K_ja?zg%R`m5*be&74Pu2m#bD(z;|oON54lD!0v;qIPc^>iBc zYh_K9u4yfDc&&3HsUl5Teeec5i&9DEpEP8b5JZokvXRhG(_r}gQW;3NX*7HPzz{r? zqCK*lv@N+fzJS8p77Odwk69~$QtHlon{FICrWe*k^Dq*3tsg0-!zi#>qMmzKy<7%S#j-nwAuX20c8(?gpE2qM1!Q= z!|L9;Ou0FbI#Q3EBuZCBy*V7^7r~*mt?PT{@Iqs`U6zONHJH7v^0T34GLBkPEKTm80y=Yn1sW`8onnam_pGgluNcB{*;4)$)iQd|LvhCApKuorDc`LucbQ>&M zBs&TKphRs9d2^@2!7BOL#|ZKQt;ywtN!=*G==r#GztwdaWZ-81=Ttw-R)os(%AymX z2J}kIC#o!oF+4$bSqD@2e7z**px(d}y`i}M_zw(6c#`-Z7{RD-M01y67w2vBm4lFF z-*2M%d`wma_t%A$s1~aGnxRj-;#R(mZazA9zU_sB=S_z_3*K_aY)b0he7Tj@=!1lb zTdVh9tj>FUafiovzUl#v`Wa52BA*diw5Ys9&u2nW^z-E7q)rHdAJ9L4e6K5TBm9ym z5$F6(-kmYHPquE`ab9UY7i!(yP&(9~_QXusMMWi+4o$ec&KC6ebE@aNwMsmp+E5a3 zZL9D0YHJT{ZEgPa$jp45DjqBkr7gM6zG?Fh-MNBg&%cXBZA*ysd3q1#yW||b4>ibs zIuRloJg?KgUOcb!G=zOq{V;4idO-|GEH|L%VSjf&eFJ;~tjmu;_(E?X&$+yTfg76! zhB{|&-c5Tef?4kVTY_2V%XPcNiadS4F9Qsw-hnSH0K$vD7=@b=&FK)p+NVAa1rsBP z%XDS%a=ZDvW7&uQcAsMF&h9tI70YM6&%4{xa>*USug_YgRB(S-+)$b|R9rd!+|{4< zN?3nd86oW%*SwET!|Gmi|`%#~|7J8#P zf3dFfp3Tj(Ii%tKJLM;YkA7*#XL~SCJmSgIw4eW3@#^Dy&U-dvdVHp4j<2=k6q$4< z(%AnuH79k!KefkJmwH^Sb(1^-v(K@v*(Q|OQ2*8E+IRXCw9WdJxHGqQSl#2~Qy7xW z_h{c+Z+@o|RP0_PPT4jl^c|hdlfVA3-g@65>Dd6d=WtH4Z=C-f*&&vM&7WZz`Vh!m zWh?9aKQ{?gsWRd-C~<0P@>#P@%mG;G*kVy_xb&IOI7-eBGsI{Hv9AGJwB$cY<>4E8E)eg!yc0Z zEX^5>rg^O=+H&tI~1cGZ%U6B1oP$F)3rL}jyseS=Pr-)i;^eeG6kXz0<1QBU{O z()eQ!y2LwfrXc#j-sln`Ge{RxChj0RKu*{9rrw*h|NVjK=vvP+FhhKue}MQ zIX~uSo{JGJjJ=_J0?tt_|GSL$AhZ8u{;G5wEJ#E)U~rci62@5>gUepLNa^Q7O>(j3dGJ5LXClr4vXsy zoa=K*$ea-*k6Em^#8#Hwsw|;N))p3W9_A8Vb>Nk#i@(D~V4#L$8?HPvOxx+Q{{zEX zV$K|ZCzY@2Z1hrskWSs;|R3P}r22y3QFIw$4rdXResuZZv~DFkN6Q zm0J3HWnIO!Pw-kLZTv_T9d{Gb^$$!>^#>$;<8kxliMN;=ha}F4&gpQ)g1Hv~`CA?v zW5ZWtYeh88bdB*q0-t6^cwN;mz~2a0@dU#DRPyk!rEL1jlA+m1_CCcVIV%qtp(zAa zg7<><(J-Pp|Fh8I6qQg9qZ7I-zzOUbIsy+Z)O&C(*lIx_bGGLjcj^k}MYz+trD*YG z0m)H&q7;RrPGOV`avvZO$EQw+`Al>R#RngG>%H=r zE~&Sb;0eM`m#1qW+wxw!)mG!5H`H0}ZZfz_#V1pP4dSZ%DXUB7R}TM+b8vsx*IWH2 zj{cxLWa;B}_)Rhc+_pKw^X^%Ea!!QyWhvu@?Ons{4mrtFaVzBx0RRIs)lqb& zEwIB*p{R4LfU4Xks=VxAQG}Cc{}Utw!A$2mv6?K)DKwBLO6F!h>d2+X0c3C+$Y!K< zDL)lnbszrIKhV)$k*MzuSsNxfz+m;BZC(sQnnL3%a$7@>gsxnZThp$|Z=m6YCzfg4 zV9gvHjxt`kY}BNtFjw6Yz;={gU&kXGp7{F5N$rK1Zx&5Cwf8rrjcRd2bS+bM;HS^8 z46Co*Bh0WugDs8+#pNoB3s=yU+f42&6=OU-#8E`rFg^~uHB1D^m=>Ly+KfigN=`XZ zMiR{lfxY1;36Wj=-H~kLq+f*@TIEByKdS6V>u1FA|`P=%LGOqT*0ALDxLP=X>$S3 z^}Jp7en+q0gJ}LE3%SVqaaUdO$r0PpoPZ^HsZxrQ~sU6V@A zVDZu}fb}oanqHve?&b^QMxIzllPR!qZ#Zks<1d3?pCiHE>3Gu8YS}tNK(@TCo_m7# z>Hg%{V!onaZ6!lmxTY@;K5xX>{efSTa^qCCu7|Wf8o^4XYBBynqzsp=XAunfnOf*> zXrmypb@uW8>R8WevgbOVTo?21;O4_1kF)tOx}I;enqucPgRKIaCRN?T)WSo|loqjM zTHKeC_mlpTUux9!+F@e0eHO&Swu)uPQ^u5Tn@rghKcCc6V+egDxZM`n%QsEOIsVK6 zY%^;S_2||}5Snij%HJWoPWzD1134(iv+i-8M5U1nL|bL?X4&u=RceBC52SN+6RUg` zh@@pEAT$%+oM3_9CGeaO;R)(fdZd2beNva~CF)|w$9>Fa9gq1#GN!k$xY3u!HTEjF zeUH7dt@kUvgeH9=*ixK-wwOSV}hTclt#mexhH zFl-5Fi(zLB>kkS1CUKHo*3;puI(leBH+m?x?$4yQz%0InZs&%c!dqQN0~pxmRDJ<0 zyZGu1ly}#>z@LJQy7<6@h0vUY$=`4zD&VsCX$H$bH{2N`{u=BzUrh`(h~ZsjM3wFw zV)md#rQxr79w_q81kHPk(*qU;iOuDsaHi-R{EJ*3FTasI_OSAK{GILR+G8(S3fWvjPH+0L`i422N#3g;c0C!F7{h!8c{0O|uGvpc0SeJ9p{w%IEI33%+ST)kX6uA5Ayu$i*!+`i}WVk6F4mbEqMm zopPmMz20c;qZOu5g(P`YZ7-~eVL_mZv5OK-&pI2XO9`p-x1cM+vXZjEXmn6G5# zjWKO{^*=CoBydx=;M9O%W4qG*IRjFND1!DvDDimw?+xivM1(L754o(Iw-tPwDr8{6 zTdxf&s&(`Ru#kHTo~49=ggnlwTe_s|QFDYGtW^cV=#y)Q5|SR$6xp1C zvP#hhNVGI(QdR9x&2ZwtE)jEcRZfO%J16~XhC9?5wo6ehUDtS#C{6zDNp*)~9iBs9 zNsQ7wJ~FMq^VlBXka{ z)@(xEpl!v+kGDD6?e+1@vnBF9hy|(((CC)1A&~-%7`j@QYVbif3+gVM|E`4UXseq5alw? zDnEDA1q=M#r=$ekUXmIbe^cJxM(3R%9 z6XcUd&2+HX;Hz0JohsU1mN|wt5A`?-8>`n!ebqa~DDC4eRU;T=!pNL>oUP`lAVe`~ zVXG*<>;j8}Pt|q&Hy<4WNc)KJD_IV`Xer!Yhdr5S;IfG^YmjNZ+`nRbU;(Jnv-Ab8 z8O#yJjj?yEdUw!l5zrp0<&%&dhrE(VEq({Z_$>goj7l_qUr#)Ux=ZdwmB00xa@JOu?ZEPI}WHhuyED4)1*5{TFWGGmWt6CJ%Vp1j3Qt=p$ zB+}Pbo64xm(<%pu;Ap?iHw60Wy>^3q<*`|*Zr5k_S@pKG((8k>X1K^L{Xqz%zuxaT z>S*s<-0F$3<+Rc5>|@YG^z~(EFyWocr{vlurG2P(E7;==OudWDC-ZnpwdqT=)k;8A z0S>`c->o4TTJ{tu;<$%ZB~gi(PiR+Af<;~K9}H`ggLncd2b-ei4n~IrM~=J$$xN>S z`#s!5bXfi_Q`YjCO~$?OyER{vt0>bx4HPJ@+M|ZnT=;W-3GU(${qR#rrw{2yLKc!{P$@wUcRQKoRjuP7+h$|^(7Qm zq!3m&t)P(7Kvxl9a7Km*oI#0*R)&SrOe0!%?V!IE(kA%_bZ(caD#@kAfv(3!*>GQs zrGMWOH-VYy@HIR82;r%v9(43jyC#Dig>hwg*pW`?gS69W%yl4!imG+ zQ1(7i?Fdlv;+CaL7$isU?RLhdZ=?BnPURY_8K-K=acZnW*e6J@^<*_bm{%$iLCt(0 z*2GCDIe*(Q$_1Di)v#F>0!CkJmog!qmK}S`*Dy43I{1oS#20epvSO!Xhjxo0d9eV@ zFZRTnt|urIr`Ctkn8TqH{+b`O%4x>+Y*GpMb9Q&^j)?-V;Jym+hDqzMI8LA?wtRIO zAD7G+k!n2^1|d06Qd+u0m$KtdR=H$mA#~G!sde>hCl0yw1u0S{r|23@y2e5H8rF+b z48h&c)Xu{tQYdewC-!-)^YB!i*2s$|by=0AQkpa#O@lJE!OsGb57E}uRfy}snPq7U zVRk9*Chtf4F?S|5hZ1LwZ%8lom<;g$Uf3)I;=H_tlZA_ptj{|z_#{^Wcq$R&WV(Xj3X*MJomMH$^yE0k%_Tz{ceHvOuU z=Ojr7xlSA}KJ@8FE~5L(p2M${A4>FYz6mX5qCnA`3JzKJLo}mM9V6tB!iCN8>F&)e z;e_0dtAs+-Ni39I8aX-j8`>83)ZdH=3+4+DKc)~R_-hnOCEqP7=R>}YF)5sH^(g~a z4QN`amp!IE!3$s7+PTA*T1)j|<|nscE~06ej?W#Oe+W8q%Bj($tvux_8Edi9p-#^T zgVs*;Wb8DCm!;L1v@^_;a@a;0T_-mf`dd$uZ#o8q;{^DsT zXiUh^Tu_-Hro{5CTGg>m2W+zZ=6REL(bzXQr*uj?dELV4YyK(44 z=bk?%K3yp<9`UI%NtFhkXlU>{C6vUqasTDYGuM5eh-Buj=N5w8*4)Y2fF9W`pARr(U8Rf) zYAB4=ed>W|8blA;((G41*Qrhih;VloY$K3PlJDX zB|%ht`w*N{P1J_rD1@H6!mh5ig3W>$#Ad|-XlKBv|HsKw1jQY2hb3M+U5d^1vd2g) zSeTS8TQsxQ@*bxGof_2qJ6@*Lxy`bSxm=SW3yFds2sZuJ@?}T=M#6AJWRnkI$sX~X zWw%>73oE5SuWBC~E5y?;Pqa6jeiEYEygq#VV#Q&> zIK)blDQ7JC;36il3>n{Iab^su(%@JN|Hg}z%s`>CF-efh$Vx7;H8(Z4l~y&2LdQ^b z8=n9-|M>&CrQp2Dr>wRKB9>FY9=sl6>-ycx;^yR{hB`puHS@_Qz`?L&`{N&&Qq#tH z-Zw5#Pu=Fc^pzl&@tCC0$;ZqNazL*CYrrMLV%=de`Q_*RHS$9CMvJ$BTx_d8?>{i> z&e^u}#f|EV?OcFA$MOd5dglIg(~K5=W=q;U)pQXcn0#_dXTsp$>S78Njhh>CLCw(;>y)ivIC&}6>p z*tk*k!n(cAYOoVm=ba4*FNIaD;d8GYqm)umP?D+c%^3|yt*SqP?W=;Y4x7y(vM8`QIVeaD8(y1Hd z$kSJc;_P0wcoMDpmgfQ4wI_c@gaV65mv2_(}!qpof(%Duy7>z zf^s0MF-~Z5OsOgBeNUj=yvMV4M79!y<}yMiY0z>t=k8P2R-LsGWlfZDDOh&kgv)O$ z9VIa>FTI6V!KbKvuyJktOV6o220d#wJmQR2Nx6ALQiK{wLCpkO$cBQXYN$7ilTG2R}m zdLsaq_5{hJIb!+z_@}7J}!4Nd6HQa>{+c`LM1U( zdZqwt+jAp%9(4<%!m+|~$fiiu*2q7wczm0vwj#RQ8eMTp0{TS6#|@0XjDasn$EWMK zZFY|YGn`qKISX)=kM~dxO!w$Y0%7V@#`CsO+Oh>nR-C9me?tKt5u}c`*A8ZWo2T{N zGYPM7(Za&->6V*|l|-p`4QS9ZsEma$CRD$1{at6IAWzyyJUy5i&Z+0muCWiCWg1keJq#6G@RGI)A^Up?j<`f53 zF0vKe!bsoskr6<|J{pmx@5gwf7JhQ^rEG7pT_jF1R2ulT6Uo z|FCjL0G&2V5Y>bYwQBilkXtBKo+qVo+__!e^yD-XS7}klwNyEQO9!~Zgt3&L>ryu) zh>VjZkK_>N?NyM|+m@o1*wwOfgSgD#4T~@Dl*Vm%J1%olA)Ra_KmMBMn^_;GK^t0m z4mbFTJ%xEV&q}YQLEj&;3OFfc&(^-g)awE43gm8`H!F)48(U*)DYv#T~A@MH_qcT}W_I0(7#Iwji(vtvmAlCYSoEvm**Z;sy zsUv;F=D3TnXkOZKpS^KdtL_+>&0A?nieb(6>CichWq!sUd#l7KY4M%RUhQWG@srb= zt7q&D_QW=XtU@@9^L|z3t9VxiPpi*zOLQp_S%N3h^j5^tDop&7d00pdN)cRRd$xIJ z3dcnvV5cdS`aDAVc40zJgMMV`XXS&kiM4Y!wlj&c=lR(9n}PCsOBYLf87*;F_VKV& zJfu&Lr^w(XIm#nCr3s?QPlV@QID+%%MB2q=s98^Erw)PT$BU}ty!hXm9c0h?GQNJP z)(pW-$BvXHai*8=&FU#ffhGNN2(|J+^7%?MeLGt{`_wJWD`x4METzV?5aU1<4zCA2 z6ME|R$ccOR!GXuNOdi;s=Z8A)^dnKwZ$%m^_&YK0SUi_94OMmIGE|cXH~j-EY55>7 z)xnUbuUPg=5*%w4{2)sM(5eeT&2bMKRpO~8wFCa5V^wa!Nzszo$gT|-k5D*UH``~K zBPKD+6`^qs)v27S8}D{}a=uSB|EdtbnEJa^s%u(o(gHgw$nFT*%8Uapm%6GP*6mp;ksTDhUdX%$T|)0eYPf{;2yD zBFO{<|3z3WuR*!{HZ^hUw{5^w-~Q2iZyZ9t;Hev9Sj@hTgG`}RAL@sv1SF3THH%eRn!Rz@;h=Y zs&D=P@sIXE#u)=D=eB=f<L5~L zrCv)L#xlo|Y&Z=S(ewt&3k>X=3C;+ejR8t13Gx^7Rd=VX<;!angwK$k=HA!xJkn9I zrK^$Vn;v6ff+*iX^M_|QyReEp0Z9U}SQ4EeOM{(`Mh5SZJp?TaHI$Fu2Uam}r0VrP zl2@sdzxp;bB;ysYJ4-$F_g2e<)?mnzpv%63 z4ktaUJ~2~m^h`RpKp^X%lOv{3k6T}{L&2EF#$WuLL{}Zi@S1V9F)*o{zbjsVAe7FO z@@7ggcyG@#gxi{bNEWiQb8fP3B6e@pAB&Fyg&*2UT41_4i{c230s^W{te2_YU&g?(b&7n2PxC#j{IijJZFkn6+_jM-teILmwuSM$8<7!6|H4FsbDO@xVB>S&s z+D;6d8p=7U<^xh4*q~25{l589` z$2d*zUGke~#;*h3ZSlvM09S9z0yK4WhkVw{+YK+*3)P|u#1LQ`t*30-X{;{Vc`h(( zGQHpUaM(ft3)0Wt=Vv#W6KW_1M6kCAcMbero-4=T^!wz9L%KXpr}uMtpF1}1@X1F3(`!D zo(hOeUNCG6iaJF1Ys?Zm`reqR3@wnxLnhlP%fQ<79jN{T|9Si4To?b`c+ZL}pz}vc z)R5-jZHNy-)P-D9-C-b4d=GImI46ykmqFiUz=3@Pdw|N7jJ60+vgwS}5FpGR%cNE= z)w-{%(|Y7=8PyI!Jt!**21^qDN;-bZ7g*>Au8)nRJJo#&nd2dEzmZqj+3HB%a245N z$#x7Ed?jAN+`0QJdW6+wPy=R(wX-g4`h;Mrc!n{oO82%{|c*2-9z5Q)e5+( z*acCOVs~BXvwzJ3t|bypR2+t&*@QN(xD6TCpXHxc1%6=**B4bO)GS~Q2(Kez+Ao2& zwiMQ&`Ewka628PB+CXD5#UI%9zYK;*MTK=6#1jK9ua&TAg-y7nQsoAtUuoHJ9Mo+@ z^-fPqD6OUs^f_ZC$*b_D(lq^e&(?ef;=mTE(@EAOibGRcz#nfg+ypYRs(0~we4+q- zI0UQf@BWKI0T9cKhJso0J6_3o6p#W^^@Du?7XfoVxAhS)6FoRou@_F1NE--9*2&As zga~Uo@>C{8uJ4ySg(i*OoBoB)U-`DfkLfM5bCnGhtv_HxX9UD@`-)_#n2ne*J{-_x zvSGZa#jro|+5et<8FdV==w}kRO8tm=R8o}rQuij23k^k$Bpt17I;NA*X3k<4R_(iV z*eg(^apegWC7Hc(*WJ0o_18^Q{R69dDYHG+oMvnGdD6Ue)SucGg?_;R00LEE-RO z;^c42bby+7Oi^>b$F2#7uBm3eEQXz&uA;E4>JSnI^$)o3(5`1R-%`&lHB#{Nj%GE4 z;^N8d%26IPQjJ#+>sMB4g3k?IWU88#eUg?a#>HFxk*#f$fn2xcR%x0k8=$0SlyU@_ z+uyUCN(}dkX9x^4Evl8!%kyloZVpA>Nn^I4-p(c_xUIPFW&2DnE7XhUAquj=YOPQ^ zv`~d4tPo@15W(jM)aKA%YuypBf+;r%&a%X zkF$EMVWRzW{N<*w5jOKEF4SIzUKgY`s`61oUMEE@V_Tq*MX8^idEr{mPcxz*m^t=7G_m{S2> zi@{_VepkUQ&G95Az--Mb<|XQ@g$b44=9L3_)J^gm>{u34<{~MaZ<tT1Je0q~@6V9N|JIO@k-| zX-3U>D(jrnx)T$~I;6qw;mDjgYNOoS*g+%gwJ!sR-S7!fJpm{hO{@jVFvSGGrLA;- zsPWx*!8u*2Q~c)Q$rz?@C`#t_C+i?3*}`C)iILZ-X3RV*WA1YG4 zHiHzQ@`V}9>h$w;783n!-8ry%%=@XJe?L3 zasp<@TyQc{3YAhyG^Q#e1tyfsv0oikL0vkIP~*Lgyg53LWKqApmmK#r?%!5iR$mbt zkl8@dsyo!FLl=`stnDMJi^aeJ;i8UT6#Z@D3qbs0faYg_HjQQa4@^SSbwPL&hk)+U z)^4)l_iHDjS;WC5;__6XdrFa<;ZQ#uZ3DHrg=SQtbrJP2O&klaGY0xZR{_myo~P@2 z%K-(Vh5Ot>8BNP1iEPKAYSdI22exB4OIMxz-a!KdWcEIi%mNf_w7l|9gU}zazH6 zh??pJW%6wIXBW+aiOc+E4nd+fe$BjJW-{Ky0DiF7Wtw4Q^Cp{7l~Ps1bTe_>az40Y zD2_4Y(5#?@&Q0tC4@!itWQ5kVg&Lm_OtuH_9%mj(!k&8NUNyChf?tty7)D^;yUaow#{#O40#P2i9Ls)_=ncHA z5;$r4F5&ZaZyOL2NU@X=lDUL3s`U>vigxj(aCq>d$!SJI#v@RcS=J%Q$Ve02_ke;v zz@JBX3&EYGA)p zyj92~hh-VxST*7}*nAZNNA9JEBc>a|J20kOpskt1M<`HE&VNHnkdf%+UzjdRv5M!O zjFMML)>#tiMEq<5Bd+vBb*Xv!LIiZgaS;BJ?CjK=73$N)u1)-|Ha_JXO-vXk&tB7( zv^s1ypLX_+%o}v{HAiJ)}6rdk!SKs|XF)On0kO z&`y&ZqO>RyY&e28o35b?Rv~MzzQZ2=MPqNAPUCB3u9-pAMk(tt!UE$IdLKuqKV(l0 zrP&-;7P8eeX)*bw9WJ3x9xcD;TizOf+Hy%^pTOWR(u?;wx~h=NenR4cZ5mOQhSPmA zGVyT?_Vx)&mNa)?;3tJcV2D{i1c)lucgfFZ{+9 zZGIfLPU4PV%=3O0OW`WM%Iy?q%1#;rY33}l17mn*u3dtqEfV3x*cX}-@==1<-7$My z?k_tC!)ZY*z)*^|6J3awri768oNbY+%&(XdQ03s~qBL&MT)j}E1LKjn%n8O=)ev>e z5dxlsqiT^9Nk*w4m(>*6qV&*!-)kj*WQ!(!mBEHdFWyC?ELTWQj$XWzT9ekbtQ(=^ z%#ZMtwKes#HUdQSV_7jW^w>VIMS5kA;RV=@!|pKT;xz6?l|REz%2EAP_^5BHDuEpZ*L~?F~Cl7J^7Nya!)# zjj%k}jTIEZ%?5e|Bx!JV@Rqs(m3s`2%;gRm~ z0RCVpTXj1$9;}}_meQ!e!06su`rx0UPQ2Y>TWbUcU>U&!5X}{X&B~Ak5sQF_Y4dF& z{M+<^YqG?vS~ACu1GmVWmR3DndsIAeL&_aWx`XH;ajjbWMbr{&q_(wQ&%2$tR`g*H zORf&kt zuxOH1fvlH(eKsf_BELA8w({bVo#M>Bm)DBt*S&zvV=u>N{s1f2hKMX>+-&i@_K{QuUMi2toSf?<(^W0Ql+Sb&3pfq|!i djQ@X6q=12$;(~#RgPDP?;%dTwE&6X({6F|iDklH{ literal 0 HcmV?d00001 diff --git a/idhub/user/views.py b/idhub/user/views.py index e6e28dc..fe6bb60 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -1,5 +1,16 @@ +import os +import base64 +import qrcode import logging +import weasyprint +import qrcode.image.svg +from io import BytesIO +from pathlib import Path +from pyhanko.sign import fields, signers +from pyhanko import stamp +from pyhanko.pdf_utils import text +from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter from django.utils.translation import gettext_lazy as _ from django.views.generic.edit import ( UpdateView, @@ -112,6 +123,105 @@ class CredentialView(MyWallet, TemplateView): class CredentialJsonView(MyWallet, TemplateView): + template_name = "certificates/4_Model_Certificat.html" + subtitle = _('Credential management') + icon = 'bi bi-patch-check-fill' + file_name = "certificate.pdf" + _pss = '123456' + + def get(self, request, *args, **kwargs): + # pk = kwargs['pk'] + # self.object = get_object_or_404( + # VerificableCredential, + # pk=pk, + # user=self.request.user + # ) + # return self.render_to_response(context=self.get_context_data()) + + data = self.build_certificate() + doc = self.insert_signature(data) + import pdb; pdb.set_trace() + response = HttpResponse(doc, content_type="application/pdf") + response['Content-Disposition'] = 'attachment; filename={}'.format(self.file_name) + return response + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + this_folder = str(Path.cwd()) + path_img_sig = "idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg" + img_signature = next(Path.cwd().glob(path_img_sig)) + with open(img_signature, 'rb') as _f: + img_sig = base64.b64encode(_f.read()).decode('utf-8') + + path_img_head = "idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg" + img_header= next(Path.cwd().glob(path_img_head)) + with open(img_header, 'rb') as _f: + img_head = base64.b64encode(_f.read()).decode('utf-8') + + qr = self.generate_qr_code("http://localhost") + context.update({ + # 'object': self.object, + "image_signature": img_sig, + "image_header": img_head, + "qr": qr + }) + return context + + def build_certificate(self): + doc = self.render_to_response(context=self.get_context_data()) + doc.render() + pdf = weasyprint.HTML(string=doc.content) + return pdf.write_pdf() + + def generate_qr_code(self, data): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(data) + qr.make(fit=True) + img_buffer = BytesIO() + img = qr.make_image(fill_color="black", back_color="white") + img.save(img_buffer, format="PNG") + + return base64.b64encode(img_buffer.getvalue()).decode('utf-8') + + def signer_init(self): + fname = "examples/signerDNIe004.pfx" + # pfx_buffer = BytesIO() + pfx_file= next(Path.cwd().glob(fname)) + s = signers.SimpleSigner.load_pkcs12( + pfx_file=pfx_file, passphrase=self._pss.encode('utf-8') + ) + return s + + def insert_signature(self, doc): + sig = self.signer_init() + _buffer = BytesIO() + _buffer.write(doc) + w = IncrementalPdfFileWriter(_buffer) + fields.append_signature_field( + w, sig_field_spec=fields.SigFieldSpec( + 'Signature', box=(150, 100, 450, 150) + ) + ) + + meta = signers.PdfSignatureMetadata(field_name='Signature') + pdf_signer = signers.PdfSigner( + meta, signer=sig, stamp_style=stamp.QRStampStyle( + stamp_text='Signed by: %(signer)s\nTime: %(ts)s\nURL: %(url)s', + text_box_style=text.TextBoxStyle() + ) + ) + _bf_out = BytesIO() + url = "https://localhost:8000/" + pdf_signer.sign_pdf(w, output=_bf_out, appearance_text_params={'url': url}) + return _bf_out.read() + + +class CredentialJsonView2(MyWallet, TemplateView): def get(self, request, *args, **kwargs): pk = kwargs['pk'] diff --git a/requirements.txt b/requirements.txt index 9b19238..23bc8ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,13 @@ jsonref==1.1.0 pyld==2.0.3 more-itertools==10.1.0 dj-database-url==2.1.0 +PyPDF2 +svg2rlg +svglib +cairosvg +pypdf +pyhanko +qrcode +uharfbuzz==0.38.0 +fontTools==4.47.0 +weasyprint==60.2 From c836112f362f3326eef6ea64cc3c1874971fe432 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 10 Jan 2024 13:53:43 +0100 Subject: [PATCH 18/70] sig credential in pdf --- idhub/admin/forms.py | 69 ++++++++++++++++++- idhub/admin/views.py | 19 ++++- idhub/migrations/0001_initial.py | 18 +++-- idhub/models.py | 3 +- .../templates/idhub/admin/wallet_issues.html | 23 +++++++ idhub/user/views.py | 65 +++++++++++++---- idhub_auth/migrations/0001_initial.py | 18 +++-- oidc4vp/migrations/0001_initial.py | 6 +- promotion/migrations/0001_initial.py | 2 +- utils/certs.py | 36 ++++++++++ 10 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 utils/certs.py diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 2649f4b..ab2f3db 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -1,11 +1,14 @@ import csv import json +import base64 import pandas as pd +from pyhanko.sign import signers + from django import forms from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError -from utils import credtools +from utils import credtools, certs from idhub.models import ( DID, File_datas, @@ -216,3 +219,67 @@ class UserRolForm(forms.ModelForm): raise forms.ValidationError(msg) return data['service'] + + +class ImportCertificateForm(forms.Form): + label = forms.CharField(label=_("Label")) + password = forms.CharField( + label=_("Password of certificate"), + widget=forms.PasswordInput + ) + file_import = forms.FileField(label=_("File import")) + + def __init__(self, *args, **kwargs): + self._did = None + self._s = None + self._label = None + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + def clean(self): + data = super().clean() + # import pdb; pdb.set_trace() + file_import = data.get('file_import') + self.pfx_file = file_import.read() + self.file_name = file_import.name + self._pss = data.get('password') + self._label = data.get('label') + if not self.pfx_file or not self._pss: + msg = _("Is not a valid certificate") + raise forms.ValidationError(msg) + + self.signer_init() + if not self._s: + msg = _("Is not a valid certificate") + raise forms.ValidationError(msg) + + self.new_did() + return data + + def new_did(self): + cert = self.pfx_file + keys = { + "cert": base64.b64encode(self.pfx_file).decode('utf-8'), + "passphrase": self._pss + } + key_material = json.dumps(keys) + self._did = DID( + key_material=key_material, + did=self.file_name, + label=self._label, + eidas1=True, + user=self.user + ) + + def save(self, commit=True): + + if commit: + self._did.save() + return self._did + + return + + def signer_init(self): + self._s = certs.load_cert( + self.pfx_file, self._pss.encode('utf-8') + ) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index b6dcbc8..7f385ab 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -30,6 +30,7 @@ from idhub.admin.forms import ( MembershipForm, SchemaForm, UserRolForm, + ImportCertificateForm, ) from idhub.admin.tables import ( DashboardTable @@ -695,11 +696,27 @@ class WalletCredentialsView(Credentials): wallet = True -class WalletConfigIssuesView(Credentials): +class WalletConfigIssuesView(Credentials, FormView): template_name = "idhub/admin/wallet_issues.html" subtitle = _('Configure credential issuance') icon = 'bi bi-patch-check-fill' wallet = True + form_class = ImportCertificateForm + success_url = reverse_lazy('idhub:admin_dids') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + cred = form.save() + if cred: + messages.success(self.request, _("The credential was imported successfully!")) + Event.set_EV_ORG_DID_CREATED_BY_ADMIN(cred) + else: + messages.error(self.request, _("Error importing the credential!")) + return super().form_valid(form) class SchemasView(SchemasMix): diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 6d87ae7..6982654 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.conf import settings from django.db import migrations, models @@ -26,9 +26,10 @@ class Migration(migrations.Migration): ), ), ('created_at', models.DateTimeField(auto_now=True)), - ('label', models.CharField(max_length=50)), + ('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.TextField()), + ('eidas1', models.BooleanField(default=False)), ( 'user', models.ForeignKey( @@ -256,8 +257,11 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('created', models.DateTimeField(auto_now=True)), - ('message', models.CharField(max_length=350)), + ('created', models.DateTimeField(auto_now=True, verbose_name='Date')), + ( + 'message', + models.CharField(max_length=350, verbose_name='Description'), + ), ( 'type', models.PositiveSmallIntegerField( @@ -295,7 +299,8 @@ class Migration(migrations.Migration): (28, 'Organisational DID deleted by admin'), (29, 'User deactivated'), (30, 'User activated'), - ] + ], + verbose_name='Event', ), ), ( @@ -327,6 +332,7 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, related_name='users', to='idhub.service', + verbose_name='Service', ), ), ( diff --git a/idhub/models.py b/idhub/models.py index 7d1ef6c..d76ea41 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -409,7 +409,8 @@ 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.TextField() + eidas1 = models.BooleanField(default=False) user = models.ForeignKey( User, on_delete=models.CASCADE, diff --git a/idhub/templates/idhub/admin/wallet_issues.html b/idhub/templates/idhub/admin/wallet_issues.html index f5849fd..b14df56 100644 --- a/idhub/templates/idhub/admin/wallet_issues.html +++ b/idhub/templates/idhub/admin/wallet_issues.html @@ -6,4 +6,27 @@ {{ subtitle }} +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} + + +
{% endblock %} diff --git a/idhub/user/views.py b/idhub/user/views.py index fe6bb60..906cf43 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -1,7 +1,9 @@ import os +import json import base64 import qrcode import logging +import datetime import weasyprint import qrcode.image.svg @@ -28,6 +30,7 @@ from idhub.user.forms import ( RequestCredentialForm, DemandAuthorizationForm ) +from utils import certs from idhub.mixins import UserView from idhub.models import DID, VerificableCredential, Event @@ -130,17 +133,16 @@ class CredentialJsonView(MyWallet, TemplateView): _pss = '123456' def get(self, request, *args, **kwargs): - # pk = kwargs['pk'] - # self.object = get_object_or_404( - # VerificableCredential, - # pk=pk, - # user=self.request.user - # ) - # return self.render_to_response(context=self.get_context_data()) + pk = kwargs['pk'] + self.user = self.request.user + self.object = get_object_or_404( + VerificableCredential, + pk=pk, + user=self.request.user + ) data = self.build_certificate() doc = self.insert_signature(data) - import pdb; pdb.set_trace() response = HttpResponse(doc, content_type="application/pdf") response['Content-Disposition'] = 'attachment; filename={}'.format(self.file_name) return response @@ -159,10 +161,31 @@ class CredentialJsonView(MyWallet, TemplateView): img_head = base64.b64encode(_f.read()).decode('utf-8') qr = self.generate_qr_code("http://localhost") + if DID.objects.filter(eidas1=True).exists(): + qr = "" + + first_name = self.user.first_name and self.user.first_name.upper() or "" + last_name = self.user.first_name and self.user.last_name.upper() or "" + document_id = "0000000-L" + course = "COURSE 1" + address = "ADDRESS" + date_course = datetime.datetime.now() + n_hours = 40 + n_lections = 5 + issue_date = datetime.datetime.now() context.update({ - # 'object': self.object, + 'object': self.object, "image_signature": img_sig, "image_header": img_head, + "first_name": first_name, + "last_name": last_name, + "document_id": document_id, + "course": course, + "address": address, + "date_course": date_course, + "n_hours": n_hours, + "n_lections": n_lections, + "issue_date": issue_date, "qr": qr }) return context @@ -188,17 +211,31 @@ class CredentialJsonView(MyWallet, TemplateView): return base64.b64encode(img_buffer.getvalue()).decode('utf-8') + def get_pfx_data(self): + did = DID.objects.filter(eidas1=True).first() + if not did: + return None, None + key_material = json.loads(did.key_material) + cert = key_material.get("cert") + passphrase = key_material.get("passphrase") + if cert and passphrase: + return base64.b64decode(cert), passphrase.encode('utf-8') + return None, None + + def signer_init(self): - fname = "examples/signerDNIe004.pfx" - # pfx_buffer = BytesIO() - pfx_file= next(Path.cwd().glob(fname)) - s = signers.SimpleSigner.load_pkcs12( - pfx_file=pfx_file, passphrase=self._pss.encode('utf-8') + pfx_data, passphrase = self.get_pfx_data() + s = certs.load_cert( + pfx_data, passphrase ) return s def insert_signature(self, doc): + # import pdb; pdb.set_trace() sig = self.signer_init() + if not sig: + return + _buffer = BytesIO() _buffer.write(doc) w = IncrementalPdfFileWriter(_buffer) diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index f460a62..41a937b 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 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.db import migrations, models @@ -31,13 +31,23 @@ class Migration(migrations.Migration): ( 'email', models.EmailField( - max_length=255, unique=True, verbose_name='email address' + max_length=255, unique=True, verbose_name='Email address' ), ), ('is_active', models.BooleanField(default=True)), ('is_admin', models.BooleanField(default=False)), - ('first_name', models.CharField(blank=True, max_length=255, null=True)), - ('last_name', models.CharField(blank=True, max_length=255, null=True)), + ( + 'first_name', + models.CharField( + blank=True, max_length=255, null=True, verbose_name='First name' + ), + ), + ( + 'last_name', + models.CharField( + blank=True, max_length=255, null=True, verbose_name='Last name' + ), + ), ], options={ 'abstract': False, diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 700c4e8..55792e2 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.conf import settings from django.db import migrations, models @@ -30,7 +30,7 @@ class Migration(migrations.Migration): 'code', models.CharField(default=oidc4vp.models.set_code, max_length=24), ), - ('code_used', models.BooleanField()), + ('code_used', models.BooleanField(default=False)), ('created', models.DateTimeField(auto_now=True)), ('presentation_definition', models.CharField(max_length=250)), ], @@ -91,7 +91,7 @@ class Migration(migrations.Migration): models.ForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='oauth2vptoken', + related_name='vp_tokens', to='oidc4vp.authorization', ), ), diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py index cbc1f17..5373e75 100644 --- a/promotion/migrations/0001_initial.py +++ b/promotion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.db import migrations, models import django.db.models.deletion diff --git a/utils/certs.py b/utils/certs.py new file mode 100644 index 0000000..ff2c241 --- /dev/null +++ b/utils/certs.py @@ -0,0 +1,36 @@ +from pyhanko.sign.signers import SimpleSigner +from cryptography.hazmat.primitives.serialization import pkcs12 +from pyhanko_certvalidator.registry import SimpleCertificateStore +from pyhanko.keys import _translate_pyca_cryptography_cert_to_asn1 +from pyhanko.keys import _translate_pyca_cryptography_key_to_asn1 + + +def load_cert(pfx_bytes, passphrase): + try: + ( + private_key, + cert, + other_certs_pkcs12, + ) = pkcs12.load_key_and_certificates(pfx_bytes, passphrase) + except (IOError, ValueError, TypeError) as e: + # logger.error( + # 'Could not load key material from PKCS#12 file', exc_info=e + # ) + return None + + kinfo = _translate_pyca_cryptography_key_to_asn1(private_key) + cert = _translate_pyca_cryptography_cert_to_asn1(cert) + other_certs_pkcs12 = set( + map(_translate_pyca_cryptography_cert_to_asn1, other_certs_pkcs12) + ) + + cs = SimpleCertificateStore() + certs_to_register = set(other_certs_pkcs12) + cs.register_multiple(certs_to_register) + return SimpleSigner( + signing_key=kinfo, + signing_cert=cert, + cert_registry=cs, + signature_mechanism=None, + prefer_pss=False, + ) From e520e42fcab1dab9b9ac74b932f5161824ea71ff Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 10 Jan 2024 13:55:43 +0100 Subject: [PATCH 19/70] add template of pdf certificate --- .../certificates/4_Model_Certificat.html | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 idhub/templates/certificates/4_Model_Certificat.html diff --git a/idhub/templates/certificates/4_Model_Certificat.html b/idhub/templates/certificates/4_Model_Certificat.html new file mode 100644 index 0000000..ae29757 --- /dev/null +++ b/idhub/templates/certificates/4_Model_Certificat.html @@ -0,0 +1,111 @@ +{% load i18n static %} + + + +Certificado + + + + + + + + + +
+
+
+ +
+
+
+
+ LAFEDE.CAT – ORGANITZACIONS PER A LA JUSTÍCIA GLOBAL
CERTIFICA QUE:
+ {{ first_name }} {{ last_name }} amb DNI {{ document_id }}
+ Ha realitzat el curs {{ course }}, a {{ address }} / de manera virtual/presencial, els dies {{ date_course }}
+ La durada del curs ha estat de {{ n_hours }} hores lectives corresponents a {{ n_lections }} sessions.
+
+
+
+ I per deixar-ne constància als efectes oportuns, signo el present certificat en data de {{ issue_date }} +
+
+ +
+
+ +
+
+ +
+
+ Pepa Martínez Peyrats
+ Directora
+ Lafede.cat - Federació d'Organitzacions per a la Justícia Global +
+
+ {% if qr %} +
+
+ +
+
+ {% endif %} +
+ + \ No newline at end of file From 139cae7c8772db2f764ff257f1b87123fe4fccb5 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 10 Jan 2024 18:35:12 +0100 Subject: [PATCH 20/70] excel or ods format for upload datas --- examples/membership-card.ods | Bin 0 -> 11886 bytes examples/membership-card.xls | Bin 0 -> 6144 bytes idhub/admin/forms.py | 3 ++- requirements.txt | 2 ++ 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 examples/membership-card.ods create mode 100644 examples/membership-card.xls diff --git a/examples/membership-card.ods b/examples/membership-card.ods new file mode 100644 index 0000000000000000000000000000000000000000..adf3cfc39e1983a28dd3b589a12319017e1092c6 GIT binary patch literal 11886 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 literal 0 HcmV?d00001 diff --git a/examples/membership-card.xls b/examples/membership-card.xls new file mode 100644 index 0000000000000000000000000000000000000000..857fb283e9381444eb17fa2b393c6ddf98697ba6 GIT binary patch literal 6144 zcmeHLU2IfU5T3i;UG73@x8+9#scVttr!Ar7K@33_3L-{e12v){(cA6aa%F#-?N-5P zTnfBNqT#_85<|iZ(I9G~51I&XK4>%=666VdBlxf=#*hH(_szYVUFt4uO`~FZ+Bs)t z&YUxI=FFLM?tcGW+ocm9FSsld%35iX+og~MtM~@+P?-*k_<)mlyHqNb*+8euedK{# zo~_(NKQ=J{;GWL_m;mx;dhLC*tZ7pl^uSKZ;x8`|*(@2fIXQscZoJG##8j(lWMp-| zXhi;AGSm7i9=MmU*kA2`=5OZlCP1z8&wS7GKMT+dmZG`l^zJ0*u`K#zL@<|XAz|9fEjWe+!pyrxnF9%PNNS( zF^Q}}!xzbQBc`?=)w)yb7Sx7(u467Lrx$VgHRMZgz2hB%fUyDj&k;*fGNBi8Q`Oty zZA5*6zn3D=QZ8pB+rkl@K@B~hyKDp=EIpPNQrH@(4=i!A*gM8!LRL{!VGQa76PT0HFl36 zvv%;2h76px(}Q+ybgwhgldy6_Z{v`5;_sYc0z?Lm8gf-$WU4Oi)O7`mOvyaU3TN!86$~)N%%(aq9C*@dqduW%F zN!XY=ZjacQZ_96K`*mD7W21JiH)ij*(jzH*B}U_laxz2CtIp7vmBRaVo4N)_+{z5w z7P?pU_Tay7Rp06oF0>ZeidlJ~CCqsMAYS1G#^U3x=Vr(79oexN&KrVLnNQl(kzu&& z=BaBxY#WTPe@XcTeLL$svV{8#Paj0jIsXbB(WUjc%FdO=jL2wzv~$3L+alZT*CIQ! zX)DvcJ)28gsm^F`Uv$GfS;0&3?qc!F!%oJEMB|54!C|eVFlM!uE9;k5;md#@>bWr9 zbD_9?c@=&FF%-fQ4gDLJZll{Mk1w}|JcqeWc~gCe)dl$=5T!Y;5US8VNSlYtm&y8pAqs7NaRh+>M*;X;ZBf*hn z-YtFBxDOc-mol)e6mTh@ZgQDw0>wv?+vf+xZFQ*tD4t1|3WDM%6e-TusvlD>MGXu# zm!bwfuw0577<4X04SaOD6g4nfU5Xm`5c@^qzL5T&Da` z(e(_7s^XKGI*!ZX8lLpE%V#tnH)^FU;(UgNlN%O3-!Rg^Fg1Dw=YbCsQd~uY#lY*Y z3Gy5oHkp=i-)k*T@y)o+N#wGl*x~_I!ymkBO4rg)f8ut$0ibu;Ps6$WF#uimEPz|S z8^FyT0&ts005tN-E%)tAybllq$lBey2a1zMiD~Ut#CMN(wO{SOsF6|CrD#3-sl#{v z+w;d)E({-g5B7K8IK2tHzy#hN@OY_kasoq)V`*zWKDwvNf5Zb*`Tei^Kj945S|-zI z!OIcP(mPST(LTIZeXh3gD>_&#wl$i6 zD5-lwP99%FS#au?WuAUs9gVJk31mO?rVPj?Xh^6Hw*CS&IeXUpjrH?7%zOH$qM5A! LtABs_xAp%C$V*a0 literal 0 HcmV?d00001 diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 2649f4b..e67789e 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -79,7 +79,8 @@ class ImportForm(forms.Form): if File_datas.objects.filter(file_name=self.file_name, success=True).exists(): raise ValidationError("This file already exists!") - df = pd.read_csv (data, delimiter="\t", quotechar='"', quoting=csv.QUOTE_ALL) + # df = pd.read_csv (data, delimiter="\t", quotechar='"', quoting=csv.QUOTE_ALL) + df = pd.read_excel(data) data_pd = df.fillna('').to_dict() if not data_pd: diff --git a/requirements.txt b/requirements.txt index 9b19238..81df537 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,8 @@ black==23.9.1 python-decouple==3.8 jsonschema==4.19.1 pandas==2.1.1 +xlrd==2.0.1 +odfpy==1.4.1 requests==2.31.0 didkit==0.3.2 jinja2==3.1.2 From 1c37368d1f3cea9cca15a4afdb40662ae67951a0 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 10 Jan 2024 19:11:22 +0100 Subject: [PATCH 21/70] fix url in id of credential --- idhub/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/idhub/models.py b/idhub/models.py index 7d1ef6c..b81f32c 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -527,8 +527,12 @@ class VerificableCredential(models.Model): format = "%Y-%m-%dT%H:%M:%SZ" issuance_date = self.issued_on.strftime(format) + url_id = "{}/credentials/{}".format( + settings.DOMAIN.strip("/"), + self.id + ) context = { - 'vc_id': self.id, + 'vc_id': url_id, 'issuer_did': self.issuer_did.did, 'subject_did': self.subject_did and self.subject_did.did or '', 'issuance_date': issuance_date, From 2df5b426106abeb935fd236574f30e760d032723 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 11 Jan 2024 12:21:32 +0100 Subject: [PATCH 22/70] add url public for get a credential with hash --- idhub/admin/forms.py | 1 - idhub/migrations/0001_initial.py | 4 ++- idhub/models.py | 11 +++++- idhub/templates/idhub/user/credential.html | 5 +++ idhub/urls.py | 6 +++- idhub/user/views.py | 39 ++++++++++++++++------ idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/migrations/0001_initial.py | 2 +- promotion/migrations/0001_initial.py | 2 +- trustchain_idhub/settings.py | 1 + 10 files changed, 56 insertions(+), 17 deletions(-) diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index d4d0a1a..8174a32 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -239,7 +239,6 @@ class ImportCertificateForm(forms.Form): def clean(self): data = super().clean() - # import pdb; pdb.set_trace() file_import = data.get('file_import') self.pfx_file = file_import.read() self.file_name = file_import.name diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 6982654..5aeabdb 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-10 11:52 +# Generated by Django 4.2.5 on 2024-01-11 10:17 from django.conf import settings from django.db import migrations, models @@ -151,6 +151,8 @@ class Migration(migrations.Migration): ('issued_on', models.DateTimeField(null=True)), ('data', models.TextField()), ('csv_data', models.TextField()), + ('public', models.BooleanField(default=True)), + ('hash', models.CharField(max_length=260)), ( 'status', models.PositiveSmallIntegerField( diff --git a/idhub/models.py b/idhub/models.py index 93f8c20..7d48543 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -1,5 +1,6 @@ import json import pytz +import hashlib import datetime from django.db import models from django.conf import settings @@ -467,6 +468,8 @@ class VerificableCredential(models.Model): issued_on = models.DateTimeField(null=True) data = models.TextField() csv_data = models.TextField() + public = models.BooleanField(default=settings.DEFAULT_PUBLIC_CREDENTIALS) + hash = models.CharField(max_length=260) status = models.PositiveSmallIntegerField( choices=Status.choices, default=Status.ENABLED @@ -520,6 +523,8 @@ class VerificableCredential(models.Model): self.render(), self.issuer_did.key_material ) + if self.public: + self.hash = hashlib.sha3_256(self.data.encode()).hexdigest() def get_context(self): d = json.loads(self.csv_data) @@ -528,8 +533,12 @@ class VerificableCredential(models.Model): format = "%Y-%m-%dT%H:%M:%SZ" issuance_date = self.issued_on.strftime(format) - url_id = "{}/credentials/{}".format( + cred_path = 'credentials' + if self.public: + cred_path = 'public/credentials' + url_id = "{}/{}/{}".format( settings.DOMAIN.strip("/"), + cred_path, self.id ) context = { diff --git a/idhub/templates/idhub/user/credential.html b/idhub/templates/idhub/user/credential.html index f1b0f42..1e0e9ca 100644 --- a/idhub/templates/idhub/user/credential.html +++ b/idhub/templates/idhub/user/credential.html @@ -37,6 +37,11 @@
+ {% if object.public %} + + {% endif %} diff --git a/idhub/urls.py b/idhub/urls.py index d139c32..1e41a5b 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -80,8 +80,12 @@ urlpatterns = [ name='user_credentials'), path('user/credentials/', views_user.CredentialView.as_view(), name='user_credential'), - path('user/credentials//json', views_user.CredentialJsonView.as_view(), + path('user/credentials//pdf', views_user.CredentialPdfView.as_view(), + name='user_credential_pdf'), + path('credentials//', views_user.CredentialJsonView.as_view(), name='user_credential_json'), + path('public/credentials//', views_user.PublicCredentialJsonView.as_view(), + name='public_credential_json'), path('user/credentials/request/', views_user.CredentialsRequestView.as_view(), name='user_credentials_request'), diff --git a/idhub/user/views.py b/idhub/user/views.py index 906cf43..b04746b 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -14,6 +14,7 @@ from pyhanko import stamp from pyhanko.pdf_utils import text from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter from django.utils.translation import gettext_lazy as _ +from django.views.generic import View from django.views.generic.edit import ( UpdateView, CreateView, @@ -25,6 +26,7 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages +from django.conf import settings from idhub.user.forms import ( ProfileForm, RequestCredentialForm, @@ -125,7 +127,7 @@ class CredentialView(MyWallet, TemplateView): return context -class CredentialJsonView(MyWallet, TemplateView): +class CredentialPdfView(MyWallet, TemplateView): template_name = "certificates/4_Model_Certificat.html" subtitle = _('Credential management') icon = 'bi bi-patch-check-fill' @@ -138,11 +140,19 @@ class CredentialJsonView(MyWallet, TemplateView): self.object = get_object_or_404( VerificableCredential, pk=pk, + public=True, user=self.request.user ) + self.url_id = "{}/public/credentials/{}".format( + settings.DOMAIN.strip("/"), + self.object.hash + ) data = self.build_certificate() - doc = self.insert_signature(data) + if DID.objects.filter(eidas1=True).exists(): + doc = self.insert_signature(data) + else: + doc = data response = HttpResponse(doc, content_type="application/pdf") response['Content-Disposition'] = 'attachment; filename={}'.format(self.file_name) return response @@ -160,9 +170,7 @@ class CredentialJsonView(MyWallet, TemplateView): with open(img_header, 'rb') as _f: img_head = base64.b64encode(_f.read()).decode('utf-8') - qr = self.generate_qr_code("http://localhost") - if DID.objects.filter(eidas1=True).exists(): - qr = "" + qr = self.generate_qr_code(self.url_id) first_name = self.user.first_name and self.user.first_name.upper() or "" last_name = self.user.first_name and self.user.last_name.upper() or "" @@ -231,7 +239,6 @@ class CredentialJsonView(MyWallet, TemplateView): return s def insert_signature(self, doc): - # import pdb; pdb.set_trace() sig = self.signer_init() if not sig: return @@ -247,18 +254,17 @@ class CredentialJsonView(MyWallet, TemplateView): meta = signers.PdfSignatureMetadata(field_name='Signature') pdf_signer = signers.PdfSigner( - meta, signer=sig, stamp_style=stamp.QRStampStyle( + meta, signer=sig, stamp_style=stamp.TextStampStyle( stamp_text='Signed by: %(signer)s\nTime: %(ts)s\nURL: %(url)s', text_box_style=text.TextBoxStyle() ) ) _bf_out = BytesIO() - url = "https://localhost:8000/" - pdf_signer.sign_pdf(w, output=_bf_out, appearance_text_params={'url': url}) + pdf_signer.sign_pdf(w, output=_bf_out, appearance_text_params={'url': self.url_id}) return _bf_out.read() -class CredentialJsonView2(MyWallet, TemplateView): +class CredentialJsonView(MyWallet, TemplateView): def get(self, request, *args, **kwargs): pk = kwargs['pk'] @@ -272,6 +278,19 @@ class CredentialJsonView2(MyWallet, TemplateView): return response +class PublicCredentialJsonView(View): + + def get(self, request, *args, **kwargs): + pk = kwargs['pk'] + self.object = get_object_or_404( + VerificableCredential, + hash=pk, + ) + response = HttpResponse(self.object.data, content_type="application/json") + response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json") + return response + + class CredentialsRequestView(MyWallet, FormView): template_name = "idhub/user/credentials_request.html" subtitle = _('Credential request') diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 41a937b..45e8f28 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-10 11:52 +# Generated by Django 4.2.5 on 2024-01-11 10:17 from django.db import migrations, models diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 55792e2..ab5b0d5 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-10 11:52 +# Generated by Django 4.2.5 on 2024-01-11 10:17 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 5373e75..2befe90 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-10 11:52 +# Generated by Django 4.2.5 on 2024-01-11 10:17 from django.db import migrations, models import django.db.models.deletion diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index b9eecc5..8230d11 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -222,3 +222,4 @@ LOGGING = { } } +DEFAULT_PUBLIC_CREDENTIALS = True From f54a6f9729b6f94c2cee6883cfac2565de90dc97 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 12 Jan 2024 14:27:19 +0100 Subject: [PATCH 23/70] . --- idhub/models.py | 12 ++++++++++++ requirements.txt | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/idhub/models.py b/idhub/models.py index 8e3785e..dd57062 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -563,6 +563,18 @@ class VerificableCredential(models.Model): return '' + def filter_dict(self, dic): + new_dict = {} + for key, value in dic.items(): + if isinstance(value, dict): + new_value = self.filter_dict(value) + if new_value: + new_dict[key] = new_value + elif value: + new_dict[key] = value + return new_dict + + class VCTemplate(models.Model): wkit_template_id = models.CharField(max_length=250) data = models.TextField() diff --git a/requirements.txt b/requirements.txt index a065a87..4aa9c3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,13 @@ black==23.9.1 python-decouple==3.8 jsonschema==4.19.1 pandas==2.1.1 +xlrd==2.0.1 +odfpy==1.4.1 requests==2.31.0 -#didkit==0.3.2 +didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 more-itertools==10.1.0 dj-database-url==2.1.0 +ujson==5.9.0 From d018c46bf4b31b9f346f789c32867fb01c14825b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 12 Jan 2024 17:22:28 +0100 Subject: [PATCH 24/70] fix problems with login --- idhub/templates/auth/login.html | 2 -- idhub/views.py | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/idhub/templates/auth/login.html b/idhub/templates/auth/login.html index 28c199e..8ae5822 100644 --- a/idhub/templates/auth/login.html +++ b/idhub/templates/auth/login.html @@ -4,8 +4,6 @@ {% block login_content %}
{% csrf_token %} - -
diff --git a/idhub/views.py b/idhub/views.py index 53db736..5f6fb71 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -13,16 +13,16 @@ class LoginView(auth_views.LoginView): } def get(self, request, *args, **kwargs): - if request.GET.get('next'): - self.extra_context['success_url'] = request.GET.get('next') + self.extra_context['success_url'] = request.GET.get( + 'next', + reverse_lazy('idhub:user_dashboard') + ) return super().get(request, *args, **kwargs) def form_valid(self, form): user = form.get_user() if not user.is_anonymous and user.is_admin: - user_dashboard = reverse_lazy('idhub:user_dashboard') admin_dashboard = reverse_lazy('idhub:admin_dashboard') - if self.extra_context['success_url'] == user_dashboard: - self.extra_context['success_url'] = admin_dashboard + self.extra_context['success_url'] = admin_dashboard auth_login(self.request, user) return HttpResponseRedirect(self.extra_context['success_url']) From eca9e6a19c335f4ada867b00708a6187179edb79 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 12 Jan 2024 17:30:27 +0100 Subject: [PATCH 25/70] fix domain in url in certificate pdf --- idhub/user/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/idhub/user/views.py b/idhub/user/views.py index b04746b..6bdbfd0 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -143,8 +143,9 @@ class CredentialPdfView(MyWallet, TemplateView): public=True, user=self.request.user ) - self.url_id = "{}/public/credentials/{}".format( - settings.DOMAIN.strip("/"), + self.url_id = "{}://{}/public/credentials/{}".format( + self.request.scheme, + self.request.get_host(), self.object.hash ) From cf49672232c61b496709d41e2de9322c9f7ccaf5 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 12 Jan 2024 17:34:45 +0100 Subject: [PATCH 26/70] fix buttons of credential --- idhub/templates/idhub/user/credential.html | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/idhub/templates/idhub/user/credential.html b/idhub/templates/idhub/user/credential.html index 1e0e9ca..0a1a3cf 100644 --- a/idhub/templates/idhub/user/credential.html +++ b/idhub/templates/idhub/user/credential.html @@ -36,16 +36,16 @@ {{ object.get_status}}
-
- {% if object.public %} - - {% endif %} - -
+
+ + {% endblock %} From 16ea7631dc18030fad3d3c104ba8f4bd1a71373c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 12 Jan 2024 20:09:19 +0100 Subject: [PATCH 27/70] filter_dict and new template membership --- idhub/models.py | 10 +- .../credentials/membership-card.json | 126 +++++++++--------- 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index dd57062..b862dc1 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -1,6 +1,8 @@ import json +import ujson import pytz import datetime +from collections import OrderedDict from django.db import models from django.conf import settings from django.template.loader import get_template @@ -525,10 +527,13 @@ class VerificableCredential(models.Model): self.status = self.Status.ISSUED self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) - self.data = sign_credential( + data = sign_credential( self.render(), self.issuer_did.key_material ) + d_ordered = ujson.loads(data) + d_minimum = self.filter_dict(d_ordered) + self.data = ujson.dumps(d_minimum) def get_context(self): d = json.loads(self.csv_data) @@ -544,6 +549,7 @@ class VerificableCredential(models.Model): 'issuance_date': issuance_date, 'first_name': self.user.first_name, 'last_name': self.user.last_name, + 'email': self.user.email } context.update(d) return context @@ -564,7 +570,7 @@ class VerificableCredential(models.Model): return '' def filter_dict(self, dic): - new_dict = {} + new_dict = OrderedDict() for key, value in dic.items(): if isinstance(value, dict): new_value = self.filter_dict(value) diff --git a/idhub/templates/credentials/membership-card.json b/idhub/templates/credentials/membership-card.json index e618833..e3d1c15 100644 --- a/idhub/templates/credentials/membership-card.json +++ b/idhub/templates/credentials/membership-card.json @@ -1,67 +1,67 @@ { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://idhub.pangea.org/credentials/base/v1", - "https://idhub.pangea.org/credentials/membership-card/v1" - ], - "type": [ - "VerifiableCredential", - "VerifiableAttestation", - "MembershipCard" - ], - "id": "https://idhub.pangea.org/credentials/987654321", - "issuer": { - "id": "did:example:5678", - "name": "Pangea Internet Solidari" + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://idhub.pangea.org/credentials/base/v1", + "https://idhub.pangea.org/credentials/membership-card/v1" + ], + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "MembershipCard" + ], + "id": "[[PLACEHOLDER]]", + "issuer": { + "id": "[[PLACEHOLDER]]", + "name": "[[PLACEHOLDER]]" + }, + "issuanceDate": "[[PLACEHOLDER]]", + "issued": "[[PLACEHOLDER]]", + "validFrom": "[[PLACEHOLDER]]", + "validUntil": "[[PLACEHOLDER]]", + "name": [ + { + "value": "Membership Card", + "lang": "en" }, - "issuanceDate": "2023-12-06T19:23:24Z", - "issued": "2023-12-06T19:23:24Z", - "validFrom": "2023-12-06T19:23:24Z", - "validUntil": "2024-12-06T19:23:24Z", - "name": [ - { - "value": "Membership Card", - "lang": "en" - }, - { - "value": "Carnet de soci/a", - "lang": "ca_ES" - }, - { - "value": "Carnet de socio/a", - "lang": "es" - } - ], - "description": [ - { - "value": "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", - "lang": "en" - }, - { - "value": "El carnet de soci especifica la subscripció o la inscripció d'un individu en serveis o beneficis específics emesos per una organització.", - "lang": "ca_ES" - }, - { - "value": "El carnet de socio especifica la suscripción o inscripción de un individuo en servicios o beneficios específicos emitidos por uns organización.", - "lang": "es" - } - ], - "credentialSubject": { - "id": "did:example:1234", - "firstName": "Joan", - "lastName": "Pera", - "email": "joan.pera@pangea.org", - "typeOfPerson": "natural", - "identityDocType": "DNI", - "identityNumber": "12345678A", - "organisation": "Pangea", - "membershipType": "individual", - "membershipId": "123456", - "affiliatedSince": "2023-01-01T00:00:00Z", - "affiliatedUntil": "2024-01-01T00:00:00Z" + { + "value": "Carnet de soci/a", + "lang": "ca_ES" }, - "credentialSchema": { - "id": "https://idhub.pangea.org/vc_schemas/membership-card.json", - "type": "FullJsonSchemaValidator2021" + { + "value": "Carnet de socio/a", + "lang": "es" } -} + ], + "description": [ + { + "value": "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", + "lang": "en" + }, + { + "value": "El carnet de soci especifica la subscripció o la inscripció d'un individu en serveis o beneficis específics emesos per una organització.", + "lang": "ca_ES" + }, + { + "value": "El carnet de socio especifica la suscripción o inscripción de un individuo en servicios o beneficios específicos emitidos por uns organización.", + "lang": "es" + } + ], + "credentialSubject": { + "id": "[[PLACEHOLDER]]", + "firstName": "[[PLACEHOLDER]]", + "lastName": "[[PLACEHOLDER]]", + "email": "[[PLACEHOLDER]]", + "typeOfPerson": "[[PLACEHOLDER]]", + "identityDocType": "[[PLACEHOLDER]]", + "identityNumber": "[[PLACEHOLDER]]", + "organisation": "[[PLACEHOLDER]]", + "membershipType": "[[PLACEHOLDER]]", + "membershipId": "[[PLACEHOLDER]]", + "affiliatedSince": "[[PLACEHOLDER]]", + "affiliatedUntil": "[[PLACEHOLDER]]" + }, + "credentialSchema": { + "id": "https://idhub.pangea.org/vc_schemas/membership-card.json", + "type": "FullJsonSchemaValidator2021" + } +} \ No newline at end of file From 336a2148f20f30644e90a9cadaab73d3a4710568 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 12 Jan 2024 21:32:26 +0100 Subject: [PATCH 28/70] fix of schema --- idhub/admin/forms.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 719696d..c54929b 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -1,5 +1,6 @@ import csv import json +import copy import pandas as pd from django import forms @@ -62,7 +63,7 @@ class ImportForm(forms.Form): self._schema = schema.first() try: self.json_schema = json.loads(self._schema.data) - props = [x for x in self.json_schema["allOf"] if 'properties' in x] + props = [x for x in self.json_schema["allOf"] if 'properties' in x.keys()] prop = props[0]['properties'] self.properties = prop['credentialSubject']['properties'] except Exception: @@ -71,7 +72,10 @@ class ImportForm(forms.Form): if not self.properties: raise ValidationError("Schema is not valid!") - + # TODO we need filter "$ref" of schema for can validate a csv + self.json_schema_filtered = copy.copy(self.json_schema) + allOf = [x for x in self.json_schema["allOf"] if '$ref' not in x.keys()] + self.json_schema_filtered["allOf"] = allOf return data def clean_file_import(self): @@ -112,10 +116,10 @@ class ImportForm(forms.Form): return def validate_jsonld(self, line, row): - import pdb; pdb.set_trace() try: - check = credtools.validate_json(row, self.json_schema) - raise ValidationError("Not valid row") + check = credtools.validate_json(row, self.json_schema_filtered) + if check is not True: + raise ValidationError("Not valid row") except Exception as e: msg = "line {}: {}".format(line+1, e) self.exception(msg) From 93c8b9812eb1cb2601529af18ae85949013a6c7c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 12 Jan 2024 21:38:23 +0100 Subject: [PATCH 29/70] fix excels --- examples/membership-card.ods | Bin 11886 -> 11038 bytes examples/membership-card.xls | Bin 6144 -> 6144 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/examples/membership-card.ods b/examples/membership-card.ods index adf3cfc39e1983a28dd3b589a12319017e1092c6..b1fe1aca9064055079330662d028d29b275fa6bf 100644 GIT binary patch delta 8965 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 delta 9860 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 HzkB}|R_M)` diff --git a/examples/membership-card.xls b/examples/membership-card.xls index 857fb283e9381444eb17fa2b393c6ddf98697ba6..bd105384bd0edb4ac47284168c35b794f5641275 100644 GIT binary patch delta 1000 zcmZuvO-vI(6#iyg`cMDdVxkv+5mZQ9KuIrxVj^OS33!pyNVl+NyRF+@Lpe|&8iN{R zoU;cF7d;ud7~;_wjS1mwV#3*j@!~-et#8^DBf8zU?|t9A_vV{7T}&^gU)=Kb90G8d z>qr%9ZGZyD%!9Lh66xA zRKjOt?ReWp2}EBgU*{R|A#Bg~vJ^4Wf!K3rsFyU!S7-IgXn`8=Vo^7A zZfk`J-JI6`Vy;kY!wXz=xJiySIWICN|zE@jx|8Ba05KYGs+mX9T zKS)XExz6Dfq6o`SY_w;R#pd(%Og92(#Uc?M7-TD9I+1)78jW<#FV!KS#DB-~#sC&; zauS5am*|Z;;75!M_(1}wf)HJbi-Lscq%H~(k~uO{c8E!BE@Fh}9$ci*QQCKqht{OB zR-KoS)Z0Xt=syyJ6xmwz5u)+CNJ>i*T2SPMQkt^jch{t}Jv*6Fnkv>~R@4i+Aj8Q7zq$*o1CC9`Y@9ST2 Cgx7)q delta 794 zcmaKpPj3=I7{;GjmM#CvpF%apXs}fjDh4nnCK{#b!B}HUltYad$1>DS*aa6*OQJyS z$y0T{L605`ma9jkm2Wau+?9R;do9CT(-<|1XI+@K}bD7-50en>QO}RV$YIz=UAhDE~E1Fbtd__Es->F*MKEG0REMA#QWOIqd zS)|l4{Xa}z3ub3truVnf``@l7;lW7DZtzkY%%I<*_8b8)dOF?-YtYb(OxOh%-UTMC z)7ch76L!OmW&eZ?82I#0{1m{p$38T5H%ZVZR3i%>EqtKaP_WQ&O~I7>8hm_Tsn9U? zH=UF!mm+9wL>iIzkX%G`xMNQz@*k3$h%R^R8AQ`E6?TGbSdGd{N|SHH$C*!d^;EwT zS!4@W*3`FNd|CR;G6M d*_>^&)A9$6XJqq8+kK8QG-Nk=OMcWp`~`sQsz(3- From c0dba1c42305a4bb4ae66068d9d2a5738b689beb Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 15 Jan 2024 10:34:42 +0100 Subject: [PATCH 30/70] Infraestructura para usar didweb --- idhub/admin/views.py | 2 +- idhub/models.py | 12 +++++++++--- idhub/urls.py | 4 +++- idhub/user/views.py | 2 +- idhub/views.py | 13 ++++++++++++- utils/idhub_ssikit/__init__.py | 26 ++++++++++++++++++++++++++ 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index b6dcbc8..690c319 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -645,7 +645,7 @@ class DidRegisterView(Credentials, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + form.instance.set_did(form.instance.type) form.save() messages.success(self.request, _('DID created successfully')) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) diff --git a/idhub/models.py b/idhub/models.py index b81f32c..eb17650 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from utils.idhub_ssikit import ( generate_did_controller_key, keydid_from_controller_key, - sign_credential, + sign_credential, webdid_from_controller_key, ) from idhub_auth.models import User @@ -416,6 +416,7 @@ class DID(models.Model): related_name='dids', null=True, ) + didweb_document = models.TextField() @property def is_organization_did(self): @@ -423,9 +424,14 @@ class DID(models.Model): return True return False - def set_did(self): + def set_did(self, type): self.key_material = generate_did_controller_key() - self.did = keydid_from_controller_key(self.key_material) + if type == "key": + self.did = keydid_from_controller_key(self.key_material) + elif type == "web": + didurl, document = webdid_from_controller_key(self.key_material) + self.did = didurl + self.didweb_document = document def get_key(self): return json.loads(self.key_material) diff --git a/idhub/urls.py b/idhub/urls.py index d139c32..8b92d93 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 +from .views import LoginView, 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 @@ -173,6 +173,8 @@ urlpatterns = [ path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), + path('did-registry/', serve_did) + # path('verification_portal/verify/', views_verification_portal.verify, # name="verification_portal_verify") ] diff --git a/idhub/user/views.py b/idhub/user/views.py index e6e28dc..50c241b 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -205,7 +205,7 @@ class DidRegisterView(MyWallet, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + form.instance.set_did(form.instance.type) form.save() messages.success(self.request, _('DID created successfully')) diff --git a/idhub/views.py b/idhub/views.py index 5f6fb71..b4fe08f 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,8 +1,12 @@ +from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy 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 -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, HttpResponse + +from idhub.models import DID +from trustchain_idhub import settings class LoginView(auth_views.LoginView): @@ -26,3 +30,10 @@ class LoginView(auth_views.LoginView): self.extra_context['success_url'] = admin_dashboard auth_login(self.request, user) return HttpResponseRedirect(self.extra_context['success_url']) + + +def serve_did(request, did_id): + document = get_object_or_404(DID, did=f'did:web:{settings.DOMAIN}:{did_id}').didweb_document + retval = HttpResponse(document) + retval.headers["Content-Type"] = "application/json" + return retval diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index cc3e9b4..84b27fd 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -6,6 +6,8 @@ import jinja2 from django.template.backends.django import Template from django.template.loader import get_template +from trustchain_idhub import settings + def generate_did_controller_key(): return didkit.generate_ed25519_key() @@ -15,6 +17,30 @@ def keydid_from_controller_key(key): return didkit.key_to_did("key", key) +async def resolve_keydid(keydid): + return await didkit.resolve_did(keydid, "{}") + + +def webdid_from_controller_key(key): + """ + Se siguen los pasos para generar un webdid a partir de un keydid. + Documentado en la docu de spruceid. + """ + keydid = keydid_from_controller_key(key) # "did:key:<...>" + pubkeyid = keydid.rsplit(":")[-1] # <...> + document = json.loads(asyncio.run(resolve_keydid(keydid))) # Documento DID en terminos "key" + webdid_url = f"did:web:{settings.DOMAIN}:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>" + webdid_url_owner = webdid_url + "#owner" + # Reemplazamos los campos del documento DID necesarios: + document["id"] = webdid_url + document["verificationMethod"]["id"] = webdid_url_owner + document["verificationMethod"]["controller"] = webdid_url + document["authentication"] = webdid_url_owner + document["assertionMethod"] = webdid_url_owner + document_fixed_serialized = json.dumps(document) + return webdid_url, document_fixed_serialized + + def generate_generic_vc_id(): # TODO agree on a system for Verifiable Credential IDs return "https://pangea.org/credentials/42" From 6b247811895d8c09464c5bad73d0b51aff6f0761 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 15 Jan 2024 19:10:07 +0100 Subject: [PATCH 31/70] add ORGANIZATION as env var --- trustchain_idhub/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index b9eecc5..6091fe0 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -222,3 +222,4 @@ LOGGING = { } } +ORGANIZATION = config('ORGANIZATION', 'Pangea') From 225c2a371207238c33e91b343b87d58ee80825fe Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 15 Jan 2024 19:10:39 +0100 Subject: [PATCH 32/70] get didkit as file in path --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4aa9c3e..13da29d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,10 +9,10 @@ pandas==2.1.1 xlrd==2.0.1 odfpy==1.4.1 requests==2.31.0 -didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 more-itertools==10.1.0 dj-database-url==2.1.0 ujson==5.9.0 +didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl From 01d9d0d1899462e4a280df78b06b0c893fe2a602 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 15 Jan 2024 19:10:55 +0100 Subject: [PATCH 33/70] fix initial_datas --- idhub/management/commands/initial_datas.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 034bc68..e4e6e4d 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -82,10 +82,18 @@ class Command(BaseCommand): try: ldata = json.loads(data) assert credtools.validate_schema(ldata) - name = ldata.get('name') - assert name + dname = ldata.get('name') + assert dname except Exception: return + name = '' + try: + for x in dname: + if settings.LANGUAGE_CODE in x['lang']: + name = x.get('value', '') + except Exception: + return + Schemas.objects.create(file_schema=file_name, data=data, type=name) def open_file(self, file_name): From 9d3b33db28cb490926cd08de20a783173c553013 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 15 Jan 2024 19:11:22 +0100 Subject: [PATCH 34/70] add new credentials --- idhub/models.py | 47 +++++++++++++------ .../credentials/membership-card.json | 38 +++++++-------- idhub/templates/idhub/admin/credentials.html | 2 +- idhub/templates/idhub/user/credentials.html | 2 +- idhub/user/forms.py | 5 +- idhub/user/views.py | 3 ++ 6 files changed, 61 insertions(+), 36 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 8d201e5..6c5ed1e 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -11,6 +11,7 @@ from utils.idhub_ssikit import ( generate_did_controller_key, keydid_from_controller_key, sign_credential, + verify_credential ) from idhub_auth.models import User @@ -509,10 +510,23 @@ class VerificableCredential(models.Model): def description(self): for des in json.loads(self.render()).get('description', []): - if settings.LANGUAGE_CODE == des.get('lang'): + if settings.LANGUAGE_CODE in des.get('lang'): return des.get('value', '') return '' + def get_type(self, lang=None): + schema = json.loads(self.schema.data) + if not schema.get('name'): + return '' + try: + for x in schema['name']: + if lang or settings.LANGUAGE_CODE in x['lang']: + return x.get('value', '') + except: + return self.schema.type + + return '' + def get_status(self): return self.Status(self.status).label @@ -524,16 +538,16 @@ class VerificableCredential(models.Model): 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) - data = sign_credential( - self.render(), + 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 ) - d_ordered = ujson.loads(data) - d_minimum = self.filter_dict(d_ordered) - self.data = ujson.dumps(d_minimum) def get_context(self): d = json.loads(self.csv_data) @@ -542,20 +556,25 @@ class VerificableCredential(models.Model): format = "%Y-%m-%dT%H:%M:%SZ" issuance_date = self.issued_on.strftime(format) - url_id = "{}/credentials/{}".format( - settings.DOMAIN.strip("/"), - self.id - ) + try: + domain = self._domain + except: + domain = settings.DOMAIN.strip("/") + + url_id = "{}/credentials/{}".format(domain, self.id) + context = { 'vc_id': url_id, 'issuer_did': self.issuer_did.did, 'subject_did': self.subject_did and self.subject_did.did or '', 'issuance_date': issuance_date, - 'first_name': self.user.first_name, - 'last_name': self.user.last_name, - 'email': self.user.email + 'firstName': self.user.first_name or "", + 'lastName': self.user.last_name or "", + 'email': self.user.email, + 'organisation': settings.ORGANIZATION or '', } context.update(d) + context['firstName'] = "" return context def render(self): diff --git a/idhub/templates/credentials/membership-card.json b/idhub/templates/credentials/membership-card.json index e3d1c15..5ecda0a 100644 --- a/idhub/templates/credentials/membership-card.json +++ b/idhub/templates/credentials/membership-card.json @@ -9,15 +9,15 @@ "VerifiableAttestation", "MembershipCard" ], - "id": "[[PLACEHOLDER]]", + "id": "{{ vc_id }}", "issuer": { - "id": "[[PLACEHOLDER]]", - "name": "[[PLACEHOLDER]]" + "id": "{{ issuer_did }}", + "name": "{{ organisation }}" }, - "issuanceDate": "[[PLACEHOLDER]]", - "issued": "[[PLACEHOLDER]]", - "validFrom": "[[PLACEHOLDER]]", - "validUntil": "[[PLACEHOLDER]]", + "issuanceDate": "{{ issuance_date }}", + "issued": "{{ issuance_date }}", + "validFrom": "{{ issuance_date }}", + "validUntil": "{{ validUntil }}", "name": [ { "value": "Membership Card", @@ -47,18 +47,18 @@ } ], "credentialSubject": { - "id": "[[PLACEHOLDER]]", - "firstName": "[[PLACEHOLDER]]", - "lastName": "[[PLACEHOLDER]]", - "email": "[[PLACEHOLDER]]", - "typeOfPerson": "[[PLACEHOLDER]]", - "identityDocType": "[[PLACEHOLDER]]", - "identityNumber": "[[PLACEHOLDER]]", - "organisation": "[[PLACEHOLDER]]", - "membershipType": "[[PLACEHOLDER]]", - "membershipId": "[[PLACEHOLDER]]", - "affiliatedSince": "[[PLACEHOLDER]]", - "affiliatedUntil": "[[PLACEHOLDER]]" + "id": "{{ subject_did }}", + "firstName": "{{ firstName }}", + "lastName": "{{ lastName }}", + "email": "{{ email }}", + "typeOfPerson": "{{ typeOfPerson }}", + "identityDocType": "{{ identityDocType }}", + "identityNumber": "{{ identityNumber }}", + "organisation": "{{ organisation }}", + "membershipType": "{{ membershipType }}", + "membershipId": "{{ vc_id }}", + "affiliatedSince": "{{ affiliatedSince }}", + "affiliatedUntil": "{{ affiliatedUntil }}" }, "credentialSchema": { "id": "https://idhub.pangea.org/vc_schemas/membership-card.json", diff --git a/idhub/templates/idhub/admin/credentials.html b/idhub/templates/idhub/admin/credentials.html index cab6a97..caee5fc 100644 --- a/idhub/templates/idhub/admin/credentials.html +++ b/idhub/templates/idhub/admin/credentials.html @@ -23,7 +23,7 @@ {% for f in credentials.all %} - {{ f.type }} + {{ f.get_type }} {{ f.description }} {{ f.get_issued_on }} {{ f.get_status }} diff --git a/idhub/templates/idhub/user/credentials.html b/idhub/templates/idhub/user/credentials.html index 6f68e56..f12ae00 100644 --- a/idhub/templates/idhub/user/credentials.html +++ b/idhub/templates/idhub/user/credentials.html @@ -22,7 +22,7 @@ {% for f in credentials.all %} - {{ f.type }} + {{ f.get_type }} {{ f.description }} {{ f.get_issued_on }} {{ f.get_status }} diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 5ac04ad..f9eda6a 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -22,12 +22,14 @@ class RequestCredentialForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) + self.lang = kwargs.pop('lang', None) + self._domain = kwargs.pop('domain', None) super().__init__(*args, **kwargs) self.fields['did'].choices = [ (x.did, x.label) for x in DID.objects.filter(user=self.user) ] self.fields['credential'].choices = [ - (x.id, x.type()) for x in VerificableCredential.objects.filter( + (x.id, x.get_type(lang=self.lang)) for x in VerificableCredential.objects.filter( user=self.user, status=VerificableCredential.Status.ENABLED ) @@ -48,6 +50,7 @@ class RequestCredentialForm(forms.Form): did = did[0] cred = cred[0] + cred._domain = self._domain try: cred.issue(did) except Exception: diff --git a/idhub/user/views.py b/idhub/user/views.py index e6e28dc..2e1258e 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -135,6 +135,9 @@ class CredentialsRequestView(MyWallet, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + kwargs['lang'] = self.request.LANGUAGE_CODE + domain = "{}://{}".format(self.request.scheme, self.request.get_host()) + kwargs['domain'] = domain return kwargs def form_valid(self, form): From 47cf19f1296c21a4069ad606edf4fed8f76e44e4 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 16 Jan 2024 14:00:05 +0100 Subject: [PATCH 35/70] add new field type in model DID --- idhub/admin/views.py | 4 ++-- idhub/models.py | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 690c319..5d8d15c 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -639,13 +639,13 @@ class DidRegisterView(Credentials, CreateView): icon = 'bi bi-patch-check-fill' wallet = True model = DID - fields = ('label',) + fields = ('label', 'type') success_url = reverse_lazy('idhub:admin_dids') object = None def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did(form.instance.type) + form.instance.set_did() form.save() messages.success(self.request, _('DID created successfully')) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) diff --git a/idhub/models.py b/idhub/models.py index eb17650..141a335 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -403,6 +403,13 @@ class Event(models.Model): class DID(models.Model): + class Types(models.IntegerChoices): + KEY = 1, "Key" + WEB = 2, "Web" + type = models.PositiveSmallIntegerField( + _("Type"), + choices=Types.choices, + ) created_at = models.DateTimeField(auto_now=True) label = models.CharField(_("Label"), max_length=50) did = models.CharField(max_length=250) @@ -424,11 +431,11 @@ class DID(models.Model): return True return False - def set_did(self, type): + def set_did(self): self.key_material = generate_did_controller_key() - if type == "key": + if self.type == self.Types.KEY: self.did = keydid_from_controller_key(self.key_material) - elif type == "web": + elif self.type == self.Types.WEB: didurl, document = webdid_from_controller_key(self.key_material) self.did = didurl self.didweb_document = document From 72a895a6de7ef397d0dcca9696aa8340a5a75dbd Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 16 Jan 2024 14:00:28 +0100 Subject: [PATCH 36/70] rebuild migrations --- idhub/migrations/0001_initial.py | 22 +++++++++++++++++----- idhub_auth/migrations/0001_initial.py | 18 ++++++++++++++---- oidc4vp/migrations/0001_initial.py | 6 +++--- promotion/migrations/0001_initial.py | 2 +- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 6d87ae7..833ae4a 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-16 11:57 from django.conf import settings from django.db import migrations, models @@ -25,10 +25,17 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), + ( + 'type', + models.PositiveSmallIntegerField( + choices=[(1, 'Web'), (2, 'Key')], verbose_name='Type' + ), + ), ('created_at', models.DateTimeField(auto_now=True)), - ('label', models.CharField(max_length=50)), + ('label', models.CharField(max_length=50, verbose_name='Label')), ('did', models.CharField(max_length=250)), ('key_material', models.CharField(max_length=250)), + ('didweb_document', models.TextField()), ( 'user', models.ForeignKey( @@ -256,8 +263,11 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('created', models.DateTimeField(auto_now=True)), - ('message', models.CharField(max_length=350)), + ('created', models.DateTimeField(auto_now=True, verbose_name='Date')), + ( + 'message', + models.CharField(max_length=350, verbose_name='Description'), + ), ( 'type', models.PositiveSmallIntegerField( @@ -295,7 +305,8 @@ class Migration(migrations.Migration): (28, 'Organisational DID deleted by admin'), (29, 'User deactivated'), (30, 'User activated'), - ] + ], + verbose_name='Event', ), ), ( @@ -327,6 +338,7 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, related_name='users', to='idhub.service', + verbose_name='Service', ), ), ( diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index f460a62..9462c05 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 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-16 11:57 from django.db import migrations, models @@ -31,13 +31,23 @@ class Migration(migrations.Migration): ( 'email', models.EmailField( - max_length=255, unique=True, verbose_name='email address' + max_length=255, unique=True, verbose_name='Email address' ), ), ('is_active', models.BooleanField(default=True)), ('is_admin', models.BooleanField(default=False)), - ('first_name', models.CharField(blank=True, max_length=255, null=True)), - ('last_name', models.CharField(blank=True, max_length=255, null=True)), + ( + 'first_name', + models.CharField( + blank=True, max_length=255, null=True, verbose_name='First name' + ), + ), + ( + 'last_name', + models.CharField( + blank=True, max_length=255, null=True, verbose_name='Last name' + ), + ), ], options={ 'abstract': False, diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 700c4e8..f57138b 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-16 11:57 from django.conf import settings from django.db import migrations, models @@ -30,7 +30,7 @@ class Migration(migrations.Migration): 'code', models.CharField(default=oidc4vp.models.set_code, max_length=24), ), - ('code_used', models.BooleanField()), + ('code_used', models.BooleanField(default=False)), ('created', models.DateTimeField(auto_now=True)), ('presentation_definition', models.CharField(max_length=250)), ], @@ -91,7 +91,7 @@ class Migration(migrations.Migration): models.ForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='oauth2vptoken', + related_name='vp_tokens', to='oidc4vp.authorization', ), ), diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py index cbc1f17..f299fd8 100644 --- a/promotion/migrations/0001_initial.py +++ b/promotion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-16 11:57 from django.db import migrations, models import django.db.models.deletion From 01bc21d1a4f5ae1c67c1a07962aae0b51b86914d Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 16 Jan 2024 14:00:59 +0100 Subject: [PATCH 37/70] add field type in form --- idhub/user/views.py | 4 ++-- idhub/views.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/idhub/user/views.py b/idhub/user/views.py index 50c241b..d715e0f 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -199,13 +199,13 @@ class DidRegisterView(MyWallet, CreateView): icon = 'bi bi-patch-check-fill' wallet = True model = DID - fields = ('label',) + fields = ('label', 'type') success_url = reverse_lazy('idhub:user_dids') object = None def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did(form.instance.type) + form.instance.set_did() form.save() messages.success(self.request, _('DID created successfully')) diff --git a/idhub/views.py b/idhub/views.py index b4fe08f..851011f 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -33,7 +33,9 @@ class LoginView(auth_views.LoginView): def serve_did(request, did_id): - document = get_object_or_404(DID, did=f'did:web:{settings.DOMAIN}:{did_id}').didweb_document + id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}' + did = get_object_or_404(DID, did=id_did) + document = did.didweb_document retval = HttpResponse(document) retval.headers["Content-Type"] = "application/json" return retval From 1453ddf4519a3f8047e25192d57d66bdf9e46c31 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 16 Jan 2024 14:01:15 +0100 Subject: [PATCH 38/70] fix some things --- idhub/management/commands/initial_datas.py | 2 +- utils/idhub_ssikit/__init__.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 034bc68..641b085 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -64,7 +64,7 @@ class Command(BaseCommand): def create_defaults_dids(self): for u in User.objects.all(): - did = DID(label="Default", user=u) + did = DID(label="Default", user=u, type=DID.Types.KEY) did.set_did() did.save() diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 84b27fd..3521eba 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -29,14 +29,14 @@ def webdid_from_controller_key(key): keydid = keydid_from_controller_key(key) # "did:key:<...>" pubkeyid = keydid.rsplit(":")[-1] # <...> document = json.loads(asyncio.run(resolve_keydid(keydid))) # Documento DID en terminos "key" - webdid_url = f"did:web:{settings.DOMAIN}:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>" + webdid_url = f"did:web:{settings.DOMAIN}:did-registry:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>" webdid_url_owner = webdid_url + "#owner" # Reemplazamos los campos del documento DID necesarios: document["id"] = webdid_url - document["verificationMethod"]["id"] = webdid_url_owner - document["verificationMethod"]["controller"] = webdid_url - document["authentication"] = webdid_url_owner - document["assertionMethod"] = webdid_url_owner + document["verificationMethod"][0]["id"] = webdid_url_owner + document["verificationMethod"][0]["controller"] = webdid_url + document["authentication"][0] = webdid_url_owner + document["assertionMethod"][0] = webdid_url_owner document_fixed_serialized = json.dumps(document) return webdid_url, document_fixed_serialized From 1e4323673c172fd560caeb3f60c24279db7e6a8b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 17 Jan 2024 12:40:54 +0100 Subject: [PATCH 39/70] encrypt admin dids with secret_key --- idhub/models.py | 5 ++++- idhub/views.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 0ff6499..128071b 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -535,7 +535,10 @@ class VerificableCredential(models.Model): self.status = self.Status.ISSUED self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) - 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) diff --git a/idhub/views.py b/idhub/views.py index e746f02..3d64501 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,4 +1,5 @@ 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 @@ -30,7 +31,11 @@ class LoginView(auth_views.LoginView): if not user.is_anonymous and user.is_admin: admin_dashboard = reverse_lazy('idhub:admin_dashboard') self.extra_context['success_url'] = admin_dashboard - cache.set("KEY_DIDS", sensitive_data_encryption_key, None) + encryption_key = user.encrypt_data( + sensitive_data_encryption_key, + settings.SECRET_KEY + ) + cache.set("KEY_DIDS", encryption_key, None) self.request.session["key_did"] = user.encrypt_data( sensitive_data_encryption_key, From 9f2abf6a04ae3a9dfd58cb193e0e758a66ef83eb Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 17 Jan 2024 13:43:40 +0100 Subject: [PATCH 40/70] fix --- idhub/models.py | 9 +++++---- idhub/views.py | 11 ++++++----- idhub_auth/models.py | 11 +++-------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 128071b..0e3fe32 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -535,10 +535,11 @@ class VerificableCredential(models.Model): self.status = self.Status.ISSUED self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) - issuer_pass = self.user.decrypt_data( - cache.get("KEY_DIDS"), - settings.SECRET_KEY, - ) + 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) diff --git a/idhub/views.py b/idhub/views.py index 3d64501..f544adb 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -31,11 +31,12 @@ class LoginView(auth_views.LoginView): if not user.is_anonymous and user.is_admin: admin_dashboard = reverse_lazy('idhub:admin_dashboard') self.extra_context['success_url'] = admin_dashboard - encryption_key = user.encrypt_data( - sensitive_data_encryption_key, - settings.SECRET_KEY - ) - cache.set("KEY_DIDS", encryption_key, None) + # 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, diff --git a/idhub_auth/models.py b/idhub_auth/models.py index 189b421..38224b2 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -135,28 +135,23 @@ class User(AbstractBaseUser): def set_encrypted_sensitive_data(self, password): key = base64.b64encode(nacl.utils.random(64)) - key_dids = cache.get("KEY_DIDS", {}) - - if key_dids.get(self.id): - key = key_dids[self.id] - else: - self.set_salt() + 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 = base64.b64encode(data.encode('utf-8')) 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')) + pw = base64.b64decode(password.encode('utf-8')*4) sb_key = self.derive_key_from_password(pw) return nacl.secret.SecretBox(sb_key) From fa3ae3dbe823d65207db8693e88ce37e835389ae Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 17 Jan 2024 18:42:15 +0100 Subject: [PATCH 41/70] fix url dids --- idhub/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idhub/urls.py b/idhub/urls.py index 5d52840..7f983c3 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -176,7 +176,7 @@ urlpatterns = [ path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), - path('did-registry/', serve_did) + path('did-registry//did.json', serve_did) # path('verification_portal/verify/', views_verification_portal.verify, # name="verification_portal_verify") From 39197bc3e5b57c842c25e2bed8f578da2c933e42 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 17 Jan 2024 18:42:44 +0100 Subject: [PATCH 42/70] rebuild migrations --- idhub/migrations/0001_initial.py | 2 +- idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/migrations/0001_initial.py | 2 +- promotion/migrations/0001_initial.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 321e70a..355af60 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-17 13:11 +# Generated by Django 4.2.5 on 2024-01-17 16:56 from django.conf import settings from django.db import migrations, models diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index ee8f4a9..84a93da 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-17 13:11 +# Generated by Django 4.2.5 on 2024-01-17 16:56 from django.db import migrations, models diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 586b4e1..b1fceed 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-17 13:11 +# Generated by Django 4.2.5 on 2024-01-17 16:56 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 1aff341..ad2ac86 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-17 13:11 +# Generated by Django 4.2.5 on 2024-01-17 16:56 from django.db import migrations, models import django.db.models.deletion From baa909ed94f81af654e2d2abe861afe4236ebff2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 18 Jan 2024 18:36:41 +0100 Subject: [PATCH 43/70] filter request credentials if admin is not validated --- idhub/mixins.py | 10 ++++++++++ idhub/templates/idhub/base.html | 2 ++ idhub/templates/idhub/user/credential.html | 2 +- idhub/user/views.py | 13 ++++++++++--- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/idhub/mixins.py b/idhub/mixins.py index 68b7344..a87d821 100644 --- a/idhub/mixins.py +++ b/idhub/mixins.py @@ -3,12 +3,21 @@ from django.contrib.auth import views as auth_views from django.urls import reverse_lazy, resolve from django.utils.translation import gettext_lazy as _ from django.shortcuts import redirect +from django.core.cache import cache class UserView(LoginRequiredMixin): login_url = "/login/" wallet = False + def get(self, request, *args, **kwargs): + self.admin_validated = cache.get("KEY_DIDS") + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.admin_validated = cache.get("KEY_DIDS") + return super().post(request, *args, **kwargs) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ @@ -19,6 +28,7 @@ class UserView(LoginRequiredMixin): 'path': resolve(self.request.path).url_name, 'user': self.request.user, 'wallet': self.wallet, + 'admin_validated': True if self.admin_validated else False }) return context diff --git a/idhub/templates/idhub/base.html b/idhub/templates/idhub/base.html index 390cb33..52f8f33 100644 --- a/idhub/templates/idhub/base.html +++ b/idhub/templates/idhub/base.html @@ -109,11 +109,13 @@ {% trans 'My credentials' %} + {% if admin_validated %} + {% endif %}