Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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"
1 change: 1 addition & 0 deletions spp_encryption/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_encryption_provider
116 changes: 116 additions & 0 deletions spp_encryption/tests/test_encryption_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from unittest.mock import patch

from odoo.tests.common import TransactionCase


class EncryptionProviderTest(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.provide_no_type = cls.env["g2p.encryption.provider"].create({"name": "Test Provider"})
cls.provider = cls.env["g2p.encryption.provider"].create(
{"name": "Test Provider with Type", "type": "jwcrypto"}
)
cls.provider_with_key = cls.env["g2p.encryption.provider"].create(
{"name": "Test Provider with Key", "type": "jwcrypto"}
)
cls.provider_with_key.generate_and_store_jwcrypto_key()

def test_get_jwk_key(self):
self.assertEqual(self.provider.jwcrypto_key, False)
with self.assertRaises(ValueError):
self.provider._get_jwk_key()

expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"]
self.assertTrue(all(elem in expected_keys for elem in self.provider_with_key._get_jwk_key().keys()))

def test_encrypt_data_jwcrypto(self):
with self.assertRaises(NotImplementedError):
self.provide_no_type.encrypt_data(b"test")

with patch.object(type(self.provider), "encrypt_data_jwcrypto", return_value=None) as encrypt_data_jwcrypto:
result = self.provider.encrypt_data(b"test")
encrypt_data_jwcrypto.assert_called_once()

result_with_key = self.provider_with_key.encrypt_data(b"test")

self.assertIsNone(result)
self.assertIsNotNone(result_with_key)
self.assertIsInstance(result_with_key, bytes)

def test_decrypt_data_jwcrypto(self):
with self.assertRaises(NotImplementedError):
self.provide_no_type.decrypt_data(b"test")

with patch.object(type(self.provider), "decrypt_data_jwcrypto", return_value=None) as decrypt_data_jwcrypto:
self.provider.decrypt_data(b"test")
decrypt_data_jwcrypto.assert_called_once()

data = "test"
encoded_data = data.encode("utf-8")

encrypted_data = self.provider_with_key.encrypt_data(encoded_data)
decrypted_data = self.provider_with_key.decrypt_data(encrypted_data)

self.assertEqual(decrypted_data, encoded_data)

def test_jwt_sign_jwcrypto(self):
with self.assertRaises(NotImplementedError):
self.provide_no_type.jwt_sign({})

with patch.object(type(self.provider), "jwt_sign_jwcrypto", return_value=None) as jwt_sign_jwcrypto:
self.provider.jwt_sign({})
jwt_sign_jwcrypto.assert_called_once()

data = {"test": "test"}
result = self.provider_with_key.jwt_sign(data)

self.assertIsNotNone(result)
self.assertIsInstance(result, str)

def test_jwt_verify_jwcrypto(self):
with self.assertRaises(NotImplementedError):
self.provide_no_type.jwt_verify("test")

with patch.object(type(self.provider), "jwt_verify_jwcrypto", return_value=None) as jwt_verify_jwcrypto:
self.provider.jwt_verify("test")
jwt_verify_jwcrypto.assert_called_once()

data = {"test": "test"}
token = self.provider_with_key.jwt_sign(data)
verified, received_jwt = self.provider_with_key.jwt_verify(token)

self.assertTrue(verified)
self.assertIsNotNone(received_jwt)

def test_get_jwks_jwcrypto(self):
with self.assertRaises(NotImplementedError):
self.provide_no_type.get_jwks()

with patch.object(type(self.provider), "get_jwks_jwcrypto", return_value=None) as get_jwks_jwcrypto:
self.provider.get_jwks()
get_jwks_jwcrypto.assert_called_once()

jwks = self.provider_with_key.get_jwks()

self.assertIsNotNone(jwks)
self.assertTrue("keys" in jwks)
self.assertIsInstance(jwks["keys"], list)
self.assertEqual(len(jwks["keys"]), 1)
self.assertIsInstance(jwks["keys"][0], dict)

expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"]
self.assertTrue(all(elem in expected_keys for elem in jwks["keys"][0].keys()))

def test_generate_and_store_jwcrypto_key(self):
with self.assertRaisesRegex(ValueError, "Unsupported key type. Currently, only 'RSA' is supported."):
self.provider.generate_and_store_jwcrypto_key(key_type="DSA")

self.assertIn(self.provider.jwcrypto_key, [False, None])

self.provider.generate_and_store_jwcrypto_key()

expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"]

self.assertIsNotNone(self.provider.jwcrypto_key)
self.assertTrue(all(elem in expected_keys for elem in self.provider_with_key._get_jwk_key().keys()))
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
Loading