Skip to content

Commit c7916b7

Browse files
Merge pull request #371 from OpenSPP/spp_encryption
VCI Issuer
2 parents 8221789 + 09da2fd commit c7916b7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1315
-0
lines changed

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ bravado_core
55
faker
66
geojson
77
jsonschema
8+
jwcrypto
89
pyjwt>=2.4.0
910
pyproj
1011
python-magic
12+
qrcode
1113
shapely
1214
simplejson
1315
swagger_spec_validator

spp_encryption/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

spp_encryption/__manifest__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
4+
{
5+
"name": "SPP Encryption",
6+
"category": "OpenSPP",
7+
"version": "17.0.1.0.0",
8+
"sequence": 1,
9+
"author": "OpenSPP.org",
10+
"website": "https://github.com/OpenSPP/openspp-modules",
11+
"license": "LGPL-3",
12+
"development_status": "Beta",
13+
"maintainers": ["jeremi", "gonzalesedwin1123"],
14+
"depends": [
15+
"g2p_encryption",
16+
],
17+
"external_dependencies": {"python": ["jwcrypto"]},
18+
"data": [
19+
"views/encryption_provider.xml",
20+
],
21+
"assets": {},
22+
"demo": [],
23+
"images": [],
24+
"application": False,
25+
"installable": True,
26+
"auto_install": False,
27+
}

spp_encryption/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import encryption_provider
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import json
2+
import uuid
3+
4+
from jwcrypto import jwe, jwk, jwt
5+
from jwcrypto.common import json_decode, json_encode
6+
from jwcrypto.jws import InvalidJWSSignature
7+
8+
from odoo import fields, models
9+
10+
11+
class JWCryptoEncryptionProvider(models.Model):
12+
_inherit = "g2p.encryption.provider"
13+
14+
type = fields.Selection(selection_add=[("jwcrypto", "JWCrypto")])
15+
16+
jwcrypto_key = fields.Char(help="JWK key in JSON format for encryption, decryption, signing, and verification")
17+
18+
def _get_jwk_key(self):
19+
self.ensure_one()
20+
if not self.jwcrypto_key:
21+
raise ValueError("JWCrypto key is not set.")
22+
return jwk.JWK.from_json(self.jwcrypto_key)
23+
24+
def encrypt_data_jwcrypto(self, data: bytes, **kwargs) -> bytes:
25+
self.ensure_one()
26+
key = self._get_jwk_key()
27+
enc = jwe.JWE(data, json_encode({"alg": "RSA-OAEP", "enc": "A256GCM"}))
28+
enc.add_recipient(key)
29+
return enc.serialize(compact=True).encode("utf-8")
30+
31+
def decrypt_data_jwcrypto(self, data: bytes, **kwargs) -> bytes:
32+
self.ensure_one()
33+
key = self._get_jwk_key()
34+
enc = jwe.JWE()
35+
enc.deserialize(data.decode("utf-8"), key=key)
36+
return enc.payload
37+
38+
def jwt_sign_jwcrypto(self, data, **kwargs) -> str:
39+
self.ensure_one()
40+
key = self._get_jwk_key()
41+
token = jwt.JWT(header={"alg": "RS256"}, claims=data)
42+
token.make_signed_token(key)
43+
return token.serialize()
44+
45+
def jwt_verify_jwcrypto(self, token: str, **kwargs):
46+
self.ensure_one()
47+
key = self._get_jwk_key()
48+
try:
49+
received_jwt = jwt.JWT(key=key, jwt=token)
50+
verified = True
51+
except InvalidJWSSignature:
52+
received_jwt = None
53+
verified = False
54+
return verified, received_jwt
55+
56+
def get_jwks_jwcrypto(self, **kwargs):
57+
self.ensure_one()
58+
key = self._get_jwk_key()
59+
public_key = key.export_public()
60+
jwks = {"keys": [json_decode(public_key)]}
61+
return jwks
62+
63+
def generate_and_store_jwcrypto_key(self, key_type="RSA", size=2048):
64+
"""
65+
Generates a new JWK (JSON Web Key) for the current record and stores it in the `jwcrypto_key` field.
66+
:param key_type: The type of key to generate, e.g., 'RSA'.
67+
:param size: The size of the key (applies to RSA keys).
68+
:return: None
69+
"""
70+
if key_type != "RSA":
71+
raise ValueError("Unsupported key type. Currently, only 'RSA' is supported.")
72+
73+
key = jwk.JWK.generate(kty=key_type, size=size)
74+
75+
kid = str(uuid.uuid4())
76+
77+
key_export = key.export()
78+
79+
export_data = json.loads(key_export)
80+
export_data["kid"] = kid
81+
82+
key_export = json.dumps(export_data)
83+
84+
# Assuming this method is called on a specific record, not on the model class itself
85+
self.jwcrypto_key = key_export

spp_encryption/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"

spp_encryption/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_encryption_provider
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from unittest.mock import patch
2+
3+
from odoo.tests.common import TransactionCase
4+
5+
6+
class EncryptionProviderTest(TransactionCase):
7+
@classmethod
8+
def setUpClass(cls):
9+
super().setUpClass()
10+
cls.provide_no_type = cls.env["g2p.encryption.provider"].create({"name": "Test Provider"})
11+
cls.provider = cls.env["g2p.encryption.provider"].create(
12+
{"name": "Test Provider with Type", "type": "jwcrypto"}
13+
)
14+
cls.provider_with_key = cls.env["g2p.encryption.provider"].create(
15+
{"name": "Test Provider with Key", "type": "jwcrypto"}
16+
)
17+
cls.provider_with_key.generate_and_store_jwcrypto_key()
18+
19+
def test_get_jwk_key(self):
20+
self.assertEqual(self.provider.jwcrypto_key, False)
21+
with self.assertRaises(ValueError):
22+
self.provider._get_jwk_key()
23+
24+
expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"]
25+
self.assertTrue(all(elem in expected_keys for elem in self.provider_with_key._get_jwk_key().keys()))
26+
27+
def test_encrypt_data_jwcrypto(self):
28+
with self.assertRaises(NotImplementedError):
29+
self.provide_no_type.encrypt_data(b"test")
30+
31+
with patch.object(type(self.provider), "encrypt_data_jwcrypto", return_value=None) as encrypt_data_jwcrypto:
32+
result = self.provider.encrypt_data(b"test")
33+
encrypt_data_jwcrypto.assert_called_once()
34+
35+
result_with_key = self.provider_with_key.encrypt_data(b"test")
36+
37+
self.assertIsNone(result)
38+
self.assertIsNotNone(result_with_key)
39+
self.assertIsInstance(result_with_key, bytes)
40+
41+
def test_decrypt_data_jwcrypto(self):
42+
with self.assertRaises(NotImplementedError):
43+
self.provide_no_type.decrypt_data(b"test")
44+
45+
with patch.object(type(self.provider), "decrypt_data_jwcrypto", return_value=None) as decrypt_data_jwcrypto:
46+
self.provider.decrypt_data(b"test")
47+
decrypt_data_jwcrypto.assert_called_once()
48+
49+
data = "test"
50+
encoded_data = data.encode("utf-8")
51+
52+
encrypted_data = self.provider_with_key.encrypt_data(encoded_data)
53+
decrypted_data = self.provider_with_key.decrypt_data(encrypted_data)
54+
55+
self.assertEqual(decrypted_data, encoded_data)
56+
57+
def test_jwt_sign_jwcrypto(self):
58+
with self.assertRaises(NotImplementedError):
59+
self.provide_no_type.jwt_sign({})
60+
61+
with patch.object(type(self.provider), "jwt_sign_jwcrypto", return_value=None) as jwt_sign_jwcrypto:
62+
self.provider.jwt_sign({})
63+
jwt_sign_jwcrypto.assert_called_once()
64+
65+
data = {"test": "test"}
66+
result = self.provider_with_key.jwt_sign(data)
67+
68+
self.assertIsNotNone(result)
69+
self.assertIsInstance(result, str)
70+
71+
def test_jwt_verify_jwcrypto(self):
72+
with self.assertRaises(NotImplementedError):
73+
self.provide_no_type.jwt_verify("test")
74+
75+
with patch.object(type(self.provider), "jwt_verify_jwcrypto", return_value=None) as jwt_verify_jwcrypto:
76+
self.provider.jwt_verify("test")
77+
jwt_verify_jwcrypto.assert_called_once()
78+
79+
data = {"test": "test"}
80+
token = self.provider_with_key.jwt_sign(data)
81+
verified, received_jwt = self.provider_with_key.jwt_verify(token)
82+
83+
self.assertTrue(verified)
84+
self.assertIsNotNone(received_jwt)
85+
86+
def test_get_jwks_jwcrypto(self):
87+
with self.assertRaises(NotImplementedError):
88+
self.provide_no_type.get_jwks()
89+
90+
with patch.object(type(self.provider), "get_jwks_jwcrypto", return_value=None) as get_jwks_jwcrypto:
91+
self.provider.get_jwks()
92+
get_jwks_jwcrypto.assert_called_once()
93+
94+
jwks = self.provider_with_key.get_jwks()
95+
96+
self.assertIsNotNone(jwks)
97+
self.assertTrue("keys" in jwks)
98+
self.assertIsInstance(jwks["keys"], list)
99+
self.assertEqual(len(jwks["keys"]), 1)
100+
self.assertIsInstance(jwks["keys"][0], dict)
101+
102+
expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"]
103+
self.assertTrue(all(elem in expected_keys for elem in jwks["keys"][0].keys()))
104+
105+
def test_generate_and_store_jwcrypto_key(self):
106+
with self.assertRaisesRegex(ValueError, "Unsupported key type. Currently, only 'RSA' is supported."):
107+
self.provider.generate_and_store_jwcrypto_key(key_type="DSA")
108+
109+
self.assertIn(self.provider.jwcrypto_key, [False, None])
110+
111+
self.provider.generate_and_store_jwcrypto_key()
112+
113+
expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"]
114+
115+
self.assertIsNotNone(self.provider.jwcrypto_key)
116+
self.assertTrue(all(elem in expected_keys for elem in self.provider_with_key._get_jwk_key().keys()))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<odoo>
3+
<record id="view_encryption_provider_form" model="ir.ui.view">
4+
<field name="name">view_encryption_provider_form</field>
5+
<field name="model">g2p.encryption.provider</field>
6+
<field name="inherit_id" ref="g2p_encryption.view_encryption_provider_form" />
7+
<field name="arch" type="xml">
8+
<xpath expr="//group[@name='Base']" position="after">
9+
<field name="jwcrypto_key" invisible="1" />
10+
<button
11+
name="generate_and_store_jwcrypto_key"
12+
string="Create Jwcrypto Key"
13+
type="object"
14+
class="oe_highlight btn-primary"
15+
invisible="jwcrypto_key"
16+
/>
17+
<button
18+
name="generate_and_store_jwcrypto_key"
19+
string="Update Jwcrypto Key"
20+
type="object"
21+
class="oe_highlight btn-primary"
22+
invisible="not jwcrypto_key"
23+
/>
24+
</xpath>
25+
</field>
26+
</record>
27+
</odoo>

spp_openid_vci/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import wizard

0 commit comments

Comments
 (0)