Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ bravado_core
faker
geojson
jsonschema
jwcrypto
pyjwt>=2.4.0
pyproj
python-magic
qrcode
shapely
simplejson
swagger_spec_validator
Expand Down
1 change: 1 addition & 0 deletions spp_encryption/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
27 changes: 27 additions & 0 deletions spp_encryption/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.


{
"name": "SPP Encryption",
"category": "OpenSPP",
"version": "17.0.1.0.0",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/openspp-modules",
"license": "LGPL-3",
"development_status": "Beta",
"maintainers": ["jeremi", "gonzalesedwin1123"],
"depends": [
"g2p_encryption",
],
"external_dependencies": {"python": ["jwcrypto"]},
"data": [
"views/encryption_provider.xml",
],
"assets": {},
"demo": [],
"images": [],
"application": False,
"installable": True,
"auto_install": False,
}
1 change: 1 addition & 0 deletions spp_encryption/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import encryption_provider
85 changes: 85 additions & 0 deletions spp_encryption/models/encryption_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json
import uuid

from jwcrypto import jwe, jwk, jwt
from jwcrypto.common import json_decode, json_encode
from jwcrypto.jws import InvalidJWSSignature

from odoo import fields, models


class JWCryptoEncryptionProvider(models.Model):
_inherit = "g2p.encryption.provider"

type = fields.Selection(selection_add=[("jwcrypto", "JWCrypto")])

jwcrypto_key = fields.Char(help="JWK key in JSON format for encryption, decryption, signing, and verification")

def _get_jwk_key(self):
self.ensure_one()
if not self.jwcrypto_key:
raise ValueError("JWCrypto key is not set.")
return jwk.JWK.from_json(self.jwcrypto_key)

def encrypt_data_jwcrypto(self, data: bytes, **kwargs) -> bytes:
self.ensure_one()
key = self._get_jwk_key()
enc = jwe.JWE(data, json_encode({"alg": "RSA-OAEP", "enc": "A256GCM"}))
enc.add_recipient(key)
return enc.serialize(compact=True).encode("utf-8")

def decrypt_data_jwcrypto(self, data: bytes, **kwargs) -> bytes:
self.ensure_one()
key = self._get_jwk_key()
enc = jwe.JWE()
enc.deserialize(data.decode("utf-8"), key=key)
return enc.payload

def jwt_sign_jwcrypto(self, data, **kwargs) -> str:
self.ensure_one()
key = self._get_jwk_key()
token = jwt.JWT(header={"alg": "RS256"}, claims=data)
token.make_signed_token(key)
return token.serialize()

def jwt_verify_jwcrypto(self, token: str, **kwargs):
self.ensure_one()
key = self._get_jwk_key()
try:
received_jwt = jwt.JWT(key=key, jwt=token)
verified = True
except InvalidJWSSignature:
received_jwt = None
verified = False
return verified, received_jwt

def get_jwks_jwcrypto(self, **kwargs):
self.ensure_one()
key = self._get_jwk_key()
public_key = key.export_public()
jwks = {"keys": [json_decode(public_key)]}
return jwks

def generate_and_store_jwcrypto_key(self, key_type="RSA", size=2048):
"""
Generates a new JWK (JSON Web Key) for the current record and stores it in the `jwcrypto_key` field.
:param key_type: The type of key to generate, e.g., 'RSA'.
:param size: The size of the key (applies to RSA keys).
:return: None
"""
if key_type != "RSA":
raise ValueError("Unsupported key type. Currently, only 'RSA' is supported.")

key = jwk.JWK.generate(kty=key_type, size=size)

kid = str(uuid.uuid4())

key_export = key.export()

export_data = json.loads(key_export)
export_data["kid"] = kid

key_export = json.dumps(export_data)

# Assuming this method is called on a specific record, not on the model class itself
self.jwcrypto_key = key_export
3 changes: 3 additions & 0 deletions spp_encryption/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
27 changes: 27 additions & 0 deletions spp_encryption/views/encryption_provider.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_encryption_provider_form" model="ir.ui.view">
<field name="name">view_encryption_provider_form</field>
<field name="model">g2p.encryption.provider</field>
<field name="inherit_id" ref="g2p_encryption.view_encryption_provider_form" />
<field name="arch" type="xml">
<xpath expr="//group[@name='Base']" position="after">
<field name="jwcrypto_key" invisible="1" />
<button
name="generate_and_store_jwcrypto_key"
string="Create Jwcrypto Key"
type="object"
class="oe_highlight btn-primary"
invisible="jwcrypto_key"
/>
<button
name="generate_and_store_jwcrypto_key"
string="Update Jwcrypto Key"
type="object"
class="oe_highlight btn-primary"
invisible="not jwcrypto_key"
/>
</xpath>
</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions spp_openid_vci/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
34 changes: 34 additions & 0 deletions spp_openid_vci/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.


{
"name": "SPP Registry OpenID VCI: Base",
"category": "OpenSPP",
"version": "17.0.1.0.0",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/openspp-modules",
"license": "LGPL-3",
"development_status": "Beta",
"maintainers": ["jeremi", "gonzalesedwin1123"],
"depends": [
"spp_encryption",
"g2p_encryption_rest_api",
"g2p_registry_base",
"g2p_openid_vci",
"g2p_openid_vci_rest_api",
],
"external_dependencies": {"python": ["qrcode"]},
"data": [
"security/ir.model.access.csv",
"wizard/vci_issuer_selection_view.xml",
"data/paperformat.xml",
"views/id_card.xml",
],
"assets": {},
"demo": [],
"images": [],
"application": False,
"installable": True,
"auto_install": False,
}
17 changes: 17 additions & 0 deletions spp_openid_vci/data/paperformat.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<odoo>
<record id="paperformat_a4" model="report.paperformat">
<field name="name">A4</field>
<field name="default" eval="False" />
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">40</field>
<field name="margin_bottom">40</field>
<field name="margin_left">20</field>
<field name="margin_right">20</field>
<field name="header_line" eval="False" />
<field name="header_spacing">35</field>
<field name="dpi">90</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions spp_openid_vci/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import res_partner
from . import vci_issuer
119 changes: 119 additions & 0 deletions spp_openid_vci/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import base64
import calendar
import json
from datetime import datetime
from io import BytesIO

import qrcode
import requests
from qrcode.image.pil import PilImage

from odoo import _, fields, models
from odoo.exceptions import UserError


class SPPRegistry(models.Model):
_inherit = "res.partner"

vc_qr_code = fields.Binary(string="VC QR Code", attachment=True)

def _validate_vci_issuer(self, vci_issuer):
if not vci_issuer:
raise UserError("No issuer found.")

if not vci_issuer.auth_sub_id_type_id:
raise UserError("No auth sub id type found in the issuer.")

if not vci_issuer.encryption_provider_id:
raise UserError("No encryption provider found in the issuer.")

def _issue_vc(self, vci_issuer):
self.ensure_one()

encryption_provider_id = vci_issuer.encryption_provider_id

reg_id = self.env["g2p.reg.id"].search(
[("partner_id", "=", self.id), ("id_type", "=", vci_issuer.auth_sub_id_type_id.id)]
)
if not reg_id:
raise UserError(f"No Registrant found with this ID Type: {vci_issuer.auth_sub_id_type_id.name}.")

web_base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url").rstrip("/")

url = f"{web_base_url}/api/v1/vci/.well-known/openid-credential-issuer/{vci_issuer.name}"

credential_issuer_response = requests.get(url)

issuer_data = credential_issuer_response.json()

credential_issuer = f"{issuer_data['credential_issuer']}/api/v1/security"
credentials_supported = issuer_data.get("credentials_supported", None)
credential_request = credentials_supported[0]

today = datetime.today()

encryption_provider_id = vci_issuer.encryption_provider_id

dict_data = {
"sub": reg_id.value,
"name": "OpenSPP",
"iat": calendar.timegm(today.timetuple()),
"scope": vci_issuer.scope,
"iss": credential_issuer,
}

signed_data = encryption_provider_id.jwt_sign(data=dict_data)

return self.env["g2p.openid.vci.issuers"].issue_vc(credential_request, signed_data)

def _create_qr_code(self, data):
qr = qrcode.QRCode(
error_correction=qrcode.constants.ERROR_CORRECT_L,
image_factory=PilImage,
box_size=10,
border=4,
)

qr.add_data(data)
qr.make(fit=True)

img = qr.make_image()

temp = BytesIO()
img.save(temp, format="PNG")
qr_img = base64.b64encode(temp.getvalue())
temp.close()

return qr_img

def registry_issue_card(self):
self.ensure_one()

form_id = self.env.ref("spp_openid_vci.issue_card_wizard").id
action = {
"name": _("Issue Card"),
"type": "ir.actions.act_window",
"view_mode": "form",
"view_id": form_id,
"view_type": "form",
"res_model": "spp.issue.card.wizard",
"target": "new",
"context": {
"default_partner_id": self.id,
},
}
return action

def _issue_vc_qr(self, vci_issuer):
self.ensure_one()

self._validate_vci_issuer(vci_issuer)

result = self._issue_vc(vci_issuer)

qr_img = self._create_qr_code(json.dumps(result))

self.vc_qr_code = qr_img

admission_form = self.env.ref("spp_openid_vci.action_generate_id_card").report_action(self)
return admission_form
13 changes: 13 additions & 0 deletions spp_openid_vci/models/vci_issuer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import api, models


class CustomOpenIDVCIssuer(models.Model):
_inherit = "g2p.openid.vci.issuers"

@api.constrains("auth_allowed_issuers", "issuer_type")
def onchange_auth_allowed_issuers(self):
for rec in self:
if not rec.auth_allowed_issuers:
args = [rec, f"set_default_auth_allowed_issuers_{rec.issuer_type}"]
if hasattr(*args):
getattr(*args)()
3 changes: 3 additions & 0 deletions spp_openid_vci/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
8 changes: 8 additions & 0 deletions spp_openid_vci/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
ir_attachment_crypto_admin_access,IR Attachment Crypto Admin Access,base.model_ir_attachment,g2p_encryption.crypto_admin,1,1,1,1
ir_attachment_admin_access,IR Attachment Admin Access,base.model_ir_attachment,g2p_registry_base.group_g2p_admin,1,1,1,1
ir_attachment_registrar_access,IR Attachment Registrar Access,base.model_ir_attachment,g2p_registry_base.group_g2p_registrar,1,1,1,1

spp_issue_card_wizard_crypto_admin,Issue Card Wizard Crypto Admin Access,spp_openid_vci.model_spp_issue_card_wizard,g2p_encryption.crypto_admin,1,1,1,1
spp_issue_card_wizard_admin,Issue Card Wizard Admin Access,spp_openid_vci.model_spp_issue_card_wizard,g2p_registry_base.group_g2p_admin,1,1,1,1
spp_issue_card_wizard_registrar,Issue Card Wizard Registrar Access,spp_openid_vci.model_spp_issue_card_wizard,g2p_registry_base.group_g2p_registrar,1,1,1,0
Loading