From 8b4c57b5d12e1baac235731d210e70751b6e1ffd Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 18 Dec 2023 21:19:40 +0100 Subject: [PATCH] [IMP] sign_oca: Set Encrypted data --- sign_oca/__manifest__.py | 1 + sign_oca/controllers/main.py | 21 +++- sign_oca/models/__init__.py | 1 + sign_oca/models/sign_oca_certificate.py | 19 ++++ sign_oca/models/sign_oca_request.py | 22 ++++- sign_oca/security/ir.model.access.csv | 2 + .../sign_oca_pdf/sign_oca_pdf.esm.js | 98 +++++++++++++++++-- .../sign_oca_pdf_portal.esm.js | 16 ++- sign_oca/views/sign_oca_certificate.xml | 55 +++++++++++ 9 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 sign_oca/models/sign_oca_certificate.py create mode 100644 sign_oca/views/sign_oca_certificate.xml diff --git a/sign_oca/__manifest__.py b/sign_oca/__manifest__.py index 8378e5be..f1d8b0a5 100644 --- a/sign_oca/__manifest__.py +++ b/sign_oca/__manifest__.py @@ -25,6 +25,7 @@ "views/sign_oca_field.xml", "views/sign_oca_role.xml", "views/sign_oca_template.xml", + "views/sign_oca_certificate.xml", "templates/assets.xml", ], "demo": [ diff --git a/sign_oca/controllers/main.py b/sign_oca/controllers/main.py index cbfd94de..140b2b0e 100644 --- a/sign_oca/controllers/main.py +++ b/sign_oca/controllers/main.py @@ -71,6 +71,21 @@ def get_sign_oca_content_access(self, signer_id, access_token): signer_sudo.request_id, "data" ).get_response(mimetype="application/pdf") + @http.route( + ["/sign_oca/certificate//"], + type="json", + auth="public", + website=True, + ) + def get_sign_oca_certificate(self, signer_id, access_token): + try: + signer_sudo = self._document_check_access( + "sign.oca.request.signer", signer_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + return signer_sudo.sign_certificate_id.data + @http.route( ["/sign_oca/info//"], type="json", @@ -92,11 +107,13 @@ def get_sign_oca_info_access(self, signer_id, access_token): auth="public", website=True, ) - def get_sign_oca_sign_access(self, signer_id, access_token, items): + def get_sign_oca_sign_access(self, signer_id, access_token, items, encrypted_data): try: signer_sudo = self._document_check_access( "sign.oca.request.signer", signer_id, access_token ) except (AccessError, MissingError): return request.redirect("/my") - return signer_sudo.action_sign(items, access_token=access_token) + return signer_sudo.action_sign( + items, encrypted_data=encrypted_data, access_token=access_token + ) diff --git a/sign_oca/models/__init__.py b/sign_oca/models/__init__.py index 1cbaaba0..e8d1c2b1 100644 --- a/sign_oca/models/__init__.py +++ b/sign_oca/models/__init__.py @@ -5,3 +5,4 @@ from . import sign_oca_role from . import sign_oca_field from . import sign_oca_request +from . import sign_oca_certificate diff --git a/sign_oca/models/sign_oca_certificate.py b/sign_oca/models/sign_oca_certificate.py new file mode 100644 index 00000000..9dae88d2 --- /dev/null +++ b/sign_oca/models/sign_oca_certificate.py @@ -0,0 +1,19 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SignOcaCertificate(models.Model): + """ + This certificate will allow us to encrypt sensitive data + We will not be able to decrypt it without the private key + """ + + _name = "sign.oca.certificate" + _description = "Sign Public Certificate" + _order = "id desc" + + name = fields.Char(required=True) + data = fields.Char() + active = fields.Boolean(default=True) diff --git a/sign_oca/models/sign_oca_request.py b/sign_oca/models/sign_oca_request.py index 910b28b7..8adeda5d 100644 --- a/sign_oca/models/sign_oca_request.py +++ b/sign_oca/models/sign_oca_request.py @@ -345,13 +345,21 @@ class SignOcaRequestSigner(models.Model): _inherit = "portal.mixin" _description = "Sign Request Value" - data = fields.Binary(related="request_id.data") + data = fields.Binary(related="request_id.data", copy=False) request_id = fields.Many2one("sign.oca.request", required=True, ondelete="cascade") partner_name = fields.Char(related="partner_id.name") partner_id = fields.Many2one("res.partner", required=True, ondelete="restrict") role_id = fields.Many2one("sign.oca.role", required=True, ondelete="restrict") - signed_on = fields.Datetime(readonly=True) - signature_hash = fields.Char(readonly=True) + signed_on = fields.Datetime(readonly=True, copy=False) + signature_hash = fields.Char(readonly=True, copy=False) + sign_certificate_id = fields.Many2one( + "sign.oca.certificate", + default=lambda r: r._get_sign_certificate(), + readonly=True, + copy=False, + ) + sensitive_data = fields.Binary(readonly=True, copy=False) + encrypted_data = fields.Json() model = fields.Char(compute="_compute_model", store=True) res_id = fields.Integer(compute="_compute_res_id", store=True) is_allow_signature = fields.Boolean(compute="_compute_is_allow_signature") @@ -388,6 +396,10 @@ def _compute_is_allow_signature(self): not item.signed_on and item.partner_id == user.partner_id ) + @api.model + def _get_sign_certificate(self): + return self.env["sign.oca.certificate"].search([], limit=1) + def _compute_access_url(self): result = super()._compute_access_url() for record in self: @@ -412,6 +424,7 @@ def get_info(self, access_token=False): "name": self.request_id.template_id.name, "items": self.request_id.signatory_data, "to_sign": self.request_id.to_sign, + "certificate_id": self.sign_certificate_id.id, "partner": { "id": self.partner_id.id, "name": self.partner_id.name, @@ -430,7 +443,7 @@ def sign(self): "url": self.access_url, } - def action_sign(self, items, access_token=False): + def action_sign(self, items, encrypted_data=False, access_token=False): self.ensure_one() if self.signed_on: raise ValidationError( @@ -439,6 +452,7 @@ def action_sign(self, items, access_token=False): if self.request_id.state != "sent": raise ValidationError(_("Request cannot be signed")) self.signed_on = fields.Datetime.now() + self.encrypted_data = encrypted_data # current_hash = self.request_id.current_hash signatory_data = self.request_id.signatory_data diff --git a/sign_oca/security/ir.model.access.csv b/sign_oca/security/ir.model.access.csv index a7530d69..6b4914f5 100644 --- a/sign_oca/security/ir.model.access.csv +++ b/sign_oca/security/ir.model.access.csv @@ -17,3 +17,5 @@ edit_sign_generate_signer,edit_sign_generate_signer,model_sign_oca_template_gene edit_sign_generate_multi,edit_sign_generate_multi,model_sign_oca_template_generate_multi,sign_oca_group_user,1,1,1,1 access_sign_request_log,access_sign_request_log,model_sign_oca_request_log,sign_oca_group_user,1,0,0,0 access_sign_request_log_admin,access_sign_request_log_admin,model_sign_oca_request_log,sign_oca_group_admin,1,1,1,1 +access_sign_certificate,access_sign_certificate,model_sign_oca_certificate,sign_oca_group_user,1,0,0,0 +edit_sign_certificate,edit_sign_certificate,model_sign_oca_certificate,base.group_system,1,1,1,0 diff --git a/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js index 1a0f1de4..e0dc1962 100644 --- a/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js +++ b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js @@ -8,6 +8,7 @@ export default class SignOcaPdf extends SignOcaPdfCommon { setup() { super.setup(...arguments); this.to_sign = false; + this.sensitiveData = {}; } async willStart() { await super.willStart(...arguments); @@ -21,6 +22,54 @@ export default class SignOcaPdf extends SignOcaPdfCommon { }); this.to_sign = this.to_sign_update; } + async _encryptSensitiveData(publicKeyBase64) { + const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), (c) => + c.charCodeAt(0) + ); + var importedPublicKey = await crypto.subtle.importKey( + "spki", + publicKeyBytes.buffer, + {name: "ECDH", namedCurve: "P-256"}, + true, + [] + ); + const privateKey = await crypto.subtle.generateKey( + { + name: "ECDH", + namedCurve: "P-256", + }, + true, + ["deriveKey"] + ); + const sharedKey = await crypto.subtle.deriveKey( + { + name: "ECDH", + public: importedPublicKey, + }, + privateKey.privateKey, + { + name: "AES-CBC", + length: 256, + }, + true, + ["encrypt", "decrypt"] + ); + const iv = crypto.getRandomValues(new Uint8Array(16)); + const encryptedBuffer = await crypto.subtle.encrypt( + { + name: "AES-CBC", + iv: iv, + }, + sharedKey, + new TextEncoder().encode(JSON.stringify(this.sensitiveData)) + ); + const publicKey = await crypto.subtle.exportKey("spki", privateKey.publicKey); + this.encryptedData = { + iv: btoa(String.fromCharCode(...iv)), + data: btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer))), + public: btoa(String.fromCharCode(...new Uint8Array(publicKey))), + }; + } renderButtons(to_sign) { var $buttons = $( core.qweb.render("oca_sign_oca.SignatureButtons", { @@ -28,15 +77,46 @@ export default class SignOcaPdf extends SignOcaPdfCommon { }) ); $buttons.on("click.o_sign_oca_button_sign", () => { - this.env.services - .rpc({ - model: this.props.model, - method: "action_sign", - args: [[this.props.res_id], this.info.items], - }) - .then(() => { - this.props.trigger("history_back"); - }); + // TODO: Add encryption here + var todoFirst = []; + this.encryptedData = {}; + if ( + Object.keys(this.sensitiveData).length > 0 && + this.info.certificate_id + ) { + todoFirst.push( + new Promise((resolve) => { + this.env.services + .rpc({ + model: "sign.oca.certificate", + method: "read", + args: [[this.info.certificate_id], ["data"]], + }) + .then((public_certificate_info) => { + this._encryptSensitiveData( + public_certificate_info[0].data + ).then(() => { + resolve(); + }); + }); + }) + ); + } + Promise.all(todoFirst).then(() => { + this.env.services + .rpc({ + model: this.props.model, + method: "action_sign", + args: [ + [this.props.res_id], + this.info.items, + this.encryptedData, + ], + }) + .then(() => { + this.props.trigger("history_back"); + }); + }); }); return $buttons; } diff --git a/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js b/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js index 3b0c2cf3..1516f0f5 100644 --- a/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js +++ b/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js @@ -12,6 +12,7 @@ export class SignOcaPdfPortal extends SignOcaPdf { setup() { super.setup(...arguments); this.signOcaFooter = useRef("sign_oca_footer"); + this.sensitiveData = {}; } async willStart() { this.info = await this.env.services.rpc({ @@ -40,7 +41,18 @@ export class SignOcaPdfPortal extends SignOcaPdf { super.postIframeFields(...arguments); this.checkFilledAll(); } - _onClickSign() { + async _onClickSign() { + this.encryptedData = false; + if (Object.keys(this.sensitiveData).length > 0 && this.info.certificate_id) { + const public_certificate_info = await this.env.services.rpc({ + route: + "/sign_oca/certificate/" + + this.props.signer_id + + "/" + + this.props.access_token, + }); + await this._encryptSensitiveData(public_certificate_info); + } this.env.services .rpc({ route: @@ -48,7 +60,7 @@ export class SignOcaPdfPortal extends SignOcaPdf { this.props.signer_id + "/" + this.props.access_token, - params: {items: this.info.items}, + params: {items: this.info.items, encrypted_data: this.encryptedData}, }) .then((action) => { // As we are on frontend env, it is not possible to use do_action(), so we diff --git a/sign_oca/views/sign_oca_certificate.xml b/sign_oca/views/sign_oca_certificate.xml new file mode 100644 index 00000000..fdf3fb5e --- /dev/null +++ b/sign_oca/views/sign_oca_certificate.xml @@ -0,0 +1,55 @@ + + + + + + sign.oca.certificate + +
+
+
+ + + + + + +
+
+
+ + + sign.oca.certificate + + + + + + + + + sign.oca.certificate + + + + + + + + + Sign Oca Certificate + sign.oca.certificate + tree,form + [] + {} + + + + Sign Oca Certificate + + + + + +