From ed088e082757cec4b0d3d1d11397a80a89c533d6 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 1 Dec 2023 10:56:29 +0100 Subject: [PATCH 1/3] [ADD] sign_biometric_oca --- requirements.txt | 2 + .../odoo/addons/sign_biometric_oca | 1 + setup/sign_biometric_oca/setup.py | 6 + sign_biometric_oca/README.rst | 76 ++++ sign_biometric_oca/__init__.py | 1 + sign_biometric_oca/__manifest__.py | 36 ++ sign_biometric_oca/data/data.xml | 7 + sign_biometric_oca/models/__init__.py | 2 + sign_biometric_oca/models/sign_oca_field.py | 13 + sign_biometric_oca/models/sign_oca_request.py | 39 ++ sign_biometric_oca/readme/CONTRIBUTORS.md | 1 + sign_biometric_oca/readme/DESCRIPTION.md | 1 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 423 ++++++++++++++++++ .../static/src/components/biometric.esm.js | 76 ++++ .../static/src/components/biometric.scss | 6 + .../biometric_signature_dialog.esm.js | 131 ++++++ .../components/biometric_signature_dialog.xml | 58 +++ .../static/src/lib/perfect-freehand.esm.js | 227 ++++++++++ 19 files changed, 1106 insertions(+) create mode 100644 requirements.txt create mode 120000 setup/sign_biometric_oca/odoo/addons/sign_biometric_oca create mode 100644 setup/sign_biometric_oca/setup.py create mode 100644 sign_biometric_oca/README.rst create mode 100644 sign_biometric_oca/__init__.py create mode 100644 sign_biometric_oca/__manifest__.py create mode 100644 sign_biometric_oca/data/data.xml create mode 100644 sign_biometric_oca/models/__init__.py create mode 100644 sign_biometric_oca/models/sign_oca_field.py create mode 100644 sign_biometric_oca/models/sign_oca_request.py create mode 100644 sign_biometric_oca/readme/CONTRIBUTORS.md create mode 100644 sign_biometric_oca/readme/DESCRIPTION.md create mode 100644 sign_biometric_oca/static/description/icon.png create mode 100644 sign_biometric_oca/static/description/index.html create mode 100644 sign_biometric_oca/static/src/components/biometric.esm.js create mode 100644 sign_biometric_oca/static/src/components/biometric.scss create mode 100644 sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js create mode 100644 sign_biometric_oca/static/src/components/biometric_signature_dialog.xml create mode 100644 sign_biometric_oca/static/src/lib/perfect-freehand.esm.js diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e9d613a0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +svglib diff --git a/setup/sign_biometric_oca/odoo/addons/sign_biometric_oca b/setup/sign_biometric_oca/odoo/addons/sign_biometric_oca new file mode 120000 index 00000000..070f6831 --- /dev/null +++ b/setup/sign_biometric_oca/odoo/addons/sign_biometric_oca @@ -0,0 +1 @@ +../../../../sign_biometric_oca \ No newline at end of file diff --git a/setup/sign_biometric_oca/setup.py b/setup/sign_biometric_oca/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/sign_biometric_oca/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/sign_biometric_oca/README.rst b/sign_biometric_oca/README.rst new file mode 100644 index 00000000..f59cfab5 --- /dev/null +++ b/sign_biometric_oca/README.rst @@ -0,0 +1,76 @@ +================== +Sign Biometric Oca +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1a1329c3d712379ddbc489e3c1499da3721b4b6ffae51187a8fd5afeb42d5bc8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsign-lightgray.png?logo=github + :target: https://github.com/OCA/sign/tree/16.0/sign_biometric_oca + :alt: OCA/sign +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sign-16-0/sign-16-0-sign_biometric_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/sign&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allow to generate biometric signatures with OCA Sign Application. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- Enric Tobella + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/sign `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sign_biometric_oca/__init__.py b/sign_biometric_oca/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/sign_biometric_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sign_biometric_oca/__manifest__.py b/sign_biometric_oca/__manifest__.py new file mode 100644 index 00000000..774e6355 --- /dev/null +++ b/sign_biometric_oca/__manifest__.py @@ -0,0 +1,36 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sign Biometric Oca", + "summary": """ + Add a new widget in order to store biometric information""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sign", + "depends": [ + "sign_oca", + ], + "data": [ + "data/data.xml", + ], + "demo": [], + "external_dependencies": {"python": ["svglib"]}, + "assets": { + "web.assets_backend": [ + "sign_biometric_oca/static/src/lib/perfect-freehand.esm.js", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.xml", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js", + "sign_biometric_oca/static/src/components/biometric.esm.js", + "sign_biometric_oca/static/src/components/biometric.scss", + ], + "web.assets_frontend": [ + "sign_biometric_oca/static/src/lib/perfect-freehand.esm.js", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.xml", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js", + "sign_biometric_oca/static/src/components/biometric.esm.js", + "sign_biometric_oca/static/src/components/biometric.scss", + ], + }, +} diff --git a/sign_biometric_oca/data/data.xml b/sign_biometric_oca/data/data.xml new file mode 100644 index 00000000..96b27f0b --- /dev/null +++ b/sign_biometric_oca/data/data.xml @@ -0,0 +1,7 @@ + + + + Biometric Signature + biometric + + diff --git a/sign_biometric_oca/models/__init__.py b/sign_biometric_oca/models/__init__.py new file mode 100644 index 00000000..d634bb75 --- /dev/null +++ b/sign_biometric_oca/models/__init__.py @@ -0,0 +1,2 @@ +from . import sign_oca_field +from . import sign_oca_request diff --git a/sign_biometric_oca/models/sign_oca_field.py b/sign_biometric_oca/models/sign_oca_field.py new file mode 100644 index 00000000..dbeaa3b1 --- /dev/null +++ b/sign_biometric_oca/models/sign_oca_field.py @@ -0,0 +1,13 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SignOcaField(models.Model): + _inherit = "sign.oca.field" + + field_type = fields.Selection( + selection_add=[("biometric", "Biometric")], + ondelete={"biometric": "set default"}, + ) diff --git a/sign_biometric_oca/models/sign_oca_request.py b/sign_biometric_oca/models/sign_oca_request.py new file mode 100644 index 00000000..c635a150 --- /dev/null +++ b/sign_biometric_oca/models/sign_oca_request.py @@ -0,0 +1,39 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from base64 import b64decode +from io import BytesIO + +from PyPDF2 import PdfFileReader +from reportlab.pdfgen import canvas +from svglib.svglib import svg2rlg + +from odoo import models + + +class SignOcaRequestSigner(models.Model): + + _inherit = "sign.oca.request.signer" + + def _get_pdf_page_biometric(self, item, box): + packet = BytesIO() + can = canvas.Canvas(packet, pagesize=(box.getWidth(), box.getHeight())) + if not item.get("value") or not item["value"].get("svg"): + return False + drawing = svg2rlg(BytesIO(b64decode(item["value"]["svg"]))) + scaling_x = item["width"] / 100 * float(box.getWidth()) / drawing.width + scaling_y = item["height"] / 100 * float(box.getHeight()) / drawing.height + + drawing.width = item["width"] / 100 * float(box.getWidth()) + drawing.height = item["height"] / 100 * float(box.getHeight()) + drawing.scale(scaling_x, scaling_y) + + drawing.drawOn( + can, + item["position_x"] / 100 * float(box.getWidth()), + (100 - item["position_y"] - item["height"]) / 100 * float(box.getHeight()), + ) + can.save() + packet.seek(0) + new_pdf = PdfFileReader(packet) + return new_pdf.getPage(0) diff --git a/sign_biometric_oca/readme/CONTRIBUTORS.md b/sign_biometric_oca/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..6295fedf --- /dev/null +++ b/sign_biometric_oca/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ + - Enric Tobella diff --git a/sign_biometric_oca/readme/DESCRIPTION.md b/sign_biometric_oca/readme/DESCRIPTION.md new file mode 100644 index 00000000..4304d29d --- /dev/null +++ b/sign_biometric_oca/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Allow to generate biometric signatures with OCA Sign Application. diff --git a/sign_biometric_oca/static/description/icon.png b/sign_biometric_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/sign_biometric_oca/static/description/index.html b/sign_biometric_oca/static/description/index.html new file mode 100644 index 00000000..6dd7025c --- /dev/null +++ b/sign_biometric_oca/static/description/index.html @@ -0,0 +1,423 @@ + + + + + +Sign Biometric Oca + + + +
+

Sign Biometric Oca

+ + +

Beta License: AGPL-3 OCA/sign Translate me on Weblate Try me on Runboat

+

Allow to generate biometric signatures with OCA Sign Application.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+
    +
  • Enric Tobella
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/sign project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sign_biometric_oca/static/src/components/biometric.esm.js b/sign_biometric_oca/static/src/components/biometric.esm.js new file mode 100644 index 00000000..cc8df31c --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric.esm.js @@ -0,0 +1,76 @@ +/** @odoo-module **/ + +import {BiometricSignatureDialog} from "./biometric_signature_dialog.esm"; +import core from "web.core"; +import {registry} from "@web/core/registry"; + +const signatureSignOca = { + uploadSignature: function (parent, item, signatureItem, data) { + item.value = data; + console.log(item); + parent.postIframeField(item); + parent.checkFilledAll(); + var next_items = _.filter( + parent.info.items, + (i) => i.tabindex > item.tabindex + ).sort((a, b) => a.tabindex - b.tabindex); + if (next_items.length > 0) { + parent.items[next_items[0].id].dispatchEvent(new Event("focus_signature")); + } + }, + generate: function (parent, item, signatureItem) { + var input = $( + core.qweb.render( + "sign_biometric_oca.sign_iframe_field_biometric_signature", + {item: item} + ) + )[0]; + if (item.role === parent.info.role) { + signatureItem[0].addEventListener("focus_signature", () => { + var signatureOptions = { + displaySignatureRatio: + signatureItem[0].clientWidth / signatureItem[0].clientHeight, + }; + parent.env.services.dialog.add(BiometricSignatureDialog, { + ...signatureOptions, + uploadSignature: (data) => + this.uploadSignature(parent, item, signatureItem, data), + }); + }); + input.addEventListener("click", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + var signatureOptions = { + displaySignatureRatio: + signatureItem[0].clientWidth / signatureItem[0].clientHeight, + }; + parent.env.services.dialog.add(BiometricSignatureDialog, { + ...signatureOptions, + uploadSignature: (data) => + this.uploadSignature(parent, item, signatureItem, data), + }); + }); + input.addEventListener("keydown", (ev) => { + if ((ev.keyCode || ev.which) !== 9) { + return true; + } + ev.preventDefault(); + var next_items = _.filter( + parent.info.items, + (i) => i.tabindex > item.tabindex && i.role === parent.info.role + ); + if (next_items.length > 0) { + ev.currentTarget.blur(); + parent.items[next_items[0].id].dispatchEvent( + new Event("focus_signature") + ); + } + }); + } + return input; + }, + check: function (item) { + return Boolean(item.value); + }, +}; +registry.category("sign_oca").add("biometric", signatureSignOca); diff --git a/sign_biometric_oca/static/src/components/biometric.scss b/sign_biometric_oca/static/src/components/biometric.scss new file mode 100644 index 00000000..4b0c601d --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric.scss @@ -0,0 +1,6 @@ +.o_sign_biometric_oca { + touch-action: none; + width: 100%; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} diff --git a/sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js b/sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js new file mode 100644 index 00000000..0d099484 --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js @@ -0,0 +1,131 @@ +/** @odoo-module **/ + +import {Component, onMounted, useRef, useState} from "@odoo/owl"; + +import {Dialog} from "@web/core/dialog/dialog"; +import {getStroke} from "@sign_biometric_oca/lib/perfect-freehand.esm"; + +const average = (a, b) => (a + b) / 2; + +function getSvgPathFromStroke(points, closed = true) { + const len = points.length; + + if (len < 4) { + return ``; + } + + let a = points[0]; + let b = points[1]; + const c = points[2]; + + let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed( + 2 + )},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average( + b[1], + c[1] + ).toFixed(2)} T`; + + for (let i = 2, max = len - 1; i < max; i++) { + a = points[i]; + b = points[i + 1]; + result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed( + 2 + )} `; + } + + if (closed) { + result += "Z"; + } + + return result; +} + +export class BiometricSignatureDialog extends Component { + setup() { + this.title = this.env._t("Adopt Your Signature"); + this.svg = useRef("BiometricSignatureBox"); + this.state = useState({current_key: 0, paths: {}, recording: false}); + onMounted(() => { + this.svg.el.style.height = + this.svg.el.clientWidth / this.props.displaySignatureRatio; + this.svg.el.setAttribute( + "viewBox", + "0 0 " + + this.svg.el.clientWidth + + " " + + parseFloat(this.svg.el.style.height) + ); + }); + } + addPoint(e, current_key) { + this.state.paths[current_key] = [ + ...this.state.paths[current_key], + [e.offsetX, e.offsetY, e.pressure, new Date().getTime()], + ]; + } + handlePointerUp(e) { + this.addPoint(e, this.state.current_key); + this.state.recording = false; + } + handlePointerDown(e) { + e.target.setPointerCapture(e.pointerId); + var current_key = this.state.current_key + 1; + this.state.current_key = current_key; + this.state.recording = true; + this.state.paths[current_key] = []; + this.addPoint(e, current_key); + } + handlePointerMove(e) { + if (!this.state.recording) { + return; + } + this.addPoint(e, this.state.current_key); + } + onClickClear() { + this.state.current_key = 0; + this.state.paths = []; + } + /** + * Upload the signature image when confirm. + * + * @private + */ + onClickConfirm() { + var result = { + paths: JSON.parse(JSON.stringify(this.state.paths)), + data: this.pathData, + width: this.svg.el.clientWidth, + height: this.svg.el.clientHeight, + }; + this.svg.el.style = {}; + this.svg.el.class = ""; + result.svg = btoa(new XMLSerializer().serializeToString(this.svg.el)); + this.props.uploadSignature(result); + this.props.close(); + } + get pathData() { + var result = []; + for (const key in this.state.paths) { + result.push( + getSvgPathFromStroke( + getStroke(this.state.paths[key], { + size: 8, + thinning: 0.5, + smoothing: 0.5, + streamline: 0.5, + }) + ) + ); + } + return result; + } + get isSignatureEmpty() { + return this.state.current_key === 0; + } +} + +BiometricSignatureDialog.template = "sign_biometric_oca.BiometricSignatureDialog"; +BiometricSignatureDialog.components = {Dialog}; +BiometricSignatureDialog.defaultProps = { + displaySignatureRatio: 3.0, +}; diff --git a/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml b/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml new file mode 100644 index 00000000..db30e979 --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml @@ -0,0 +1,58 @@ + + + + + +
+ + + +
By clicking Adopt & Sign, I agree that the chosen signature/initials will be a valid electronic representation of my hand-written signature/initials for all purposes when it is used on documents, including legally binding contracts.
+
+ + + + + +
+
+ + + + + +
+ + diff --git a/sign_biometric_oca/static/src/lib/perfect-freehand.esm.js b/sign_biometric_oca/static/src/lib/perfect-freehand.esm.js new file mode 100644 index 00000000..e808fc0b --- /dev/null +++ b/sign_biometric_oca/static/src/lib/perfect-freehand.esm.js @@ -0,0 +1,227 @@ +/** @odoo-module **/ + +function $(e, t, u, x = (h) => h) { + return e * x(0.5 - t * (0.5 - u)); +} +function se(e) { + return [-e[0], -e[1]]; +} +function l(e, t) { + return [e[0] + t[0], e[1] + t[1]]; +} +function a(e, t) { + return [e[0] - t[0], e[1] - t[1]]; +} +function b(e, t) { + return [e[0] * t, e[1] * t]; +} +function he(e, t) { + return [e[0] / t, e[1] / t]; +} +function R(e) { + return [e[1], -e[0]]; +} +function B(e, t) { + return e[0] * t[0] + e[1] * t[1]; +} +function ue(e, t) { + return e[0] === t[0] && e[1] === t[1]; +} +function ge(e) { + return Math.hypot(e[0], e[1]); +} +function de(e) { + return e[0] * e[0] + e[1] * e[1]; +} +function A(e, t) { + return de(a(e, t)); +} +function G(e) { + return he(e, ge(e)); +} +function ie(e, t) { + return Math.hypot(e[1] - t[1], e[0] - t[0]); +} +function L(e, t, u) { + const x = Math.sin(u), + h = Math.cos(u), + y = e[0] - t[0], + n = e[1] - t[1], + f = y * h - n * x, + d = y * x + n * h; + return [f + t[0], d + t[1]]; +} +function K(e, t, u) { + return l(e, b(a(t, e), u)); +} +function ee(e, t, u) { + return l(e, b(t, u)); +} +var {min: C, PI: xe} = Math, + pe = 0.275, + V = xe + 1e-4; +function ce(e, t = {}) { + let { + size: u = 16, + smoothing: x = 0.5, + thinning: h = 0.5, + simulatePressure: y = !0, + easing: n = (r) => r, + start: f = {}, + end: d = {}, + last: D = !1, + } = t, + {cap: S = !0, easing: j = (r) => r * (2 - r)} = f, + {cap: q = !0, easing: c = (r) => --r * r * r + 1} = d; + if (e.length === 0 || u <= 0) return []; + let p = e[e.length - 1].runningLength, + g = f.taper === !1 ? 0 : f.taper === !0 ? Math.max(u, p) : f.taper, + T = d.taper === !1 ? 0 : d.taper === !0 ? Math.max(u, p) : d.taper, + te = Math.pow(u * x, 2), + _ = [], + M = [], + H = e.slice(0, 10).reduce((r, i) => { + let o = i.pressure; + if (y) { + const s = C(1, i.distance / u), + W = C(1, 1 - s); + o = C(1, r + (W - r) * (s * pe)); + } + return (r + o) / 2; + }, e[0].pressure), + m = $(u, h, e[e.length - 1].pressure, n), + U, + X = e[0].vector, + z = e[0].point, + F = z, + O = z, + E = F, + J = !1; + for (let r = 0; r < e.length; r++) { + let {pressure: i} = e[r], + {point: o, vector: s, distance: W, runningLength: I} = e[r]; + if (r < e.length - 1 && p - I < 3) continue; + if (h) { + if (y) { + const v = C(1, W / u), + Z = C(1, 1 - v); + i = C(1, H + (Z - H) * (v * pe)); + } + m = $(u, h, i, n); + } else m = u / 2; + U === void 0 && (U = m); + const le = I < g ? j(I / g) : 1, + fe = p - I < T ? c((p - I) / T) : 1; + m = Math.max(0.01, m * Math.min(le, fe)); + const re = (r < e.length - 1 ? e[r + 1] : e[r]).vector, + Y = r < e.length - 1 ? B(s, re) : 1, + be = B(s, X) < 0 && !J, + ne = Y !== null && Y < 0; + if (be || ne) { + const v = b(R(X), m); + for (let Z = 1 / 13, w = 0; w <= 1; w += Z) + (O = L(a(o, v), o, V * w)), _.push(O), (E = L(l(o, v), o, V * -w)), M.push(E); + (z = O), (F = E), ne && (J = !0); + continue; + } + if (((J = !1), r === e.length - 1)) { + const v = b(R(s), m); + _.push(a(o, v)), M.push(l(o, v)); + continue; + } + const oe = b(R(K(re, s, Y)), m); + (O = a(o, oe)), + (r <= 1 || A(z, O) > te) && (_.push(O), (z = O)), + (E = l(o, oe)), + (r <= 1 || A(F, E) > te) && (M.push(E), (F = E)), + (H = i), + (X = s); + } + const P = e[0].point.slice(0, 2), + k = e.length > 1 ? e[e.length - 1].point.slice(0, 2) : l(e[0].point, [1, 1]), + Q = [], + N = []; + if (e.length === 1) { + if (!(g || T) || D) { + const r = ee(P, G(R(a(P, k))), -(U || m)), + i = []; + for (let o = 1 / 13, s = o; s <= 1; s += o) i.push(L(r, P, V * 2 * s)); + return i; + } + } else { + if (!(g || (T && e.length === 1))) + if (S) + for (let i = 1 / 13, o = i; o <= 1; o += i) { + const s = L(M[0], P, V * o); + Q.push(s); + } + else { + const i = a(_[0], M[0]), + o = b(i, 0.5), + s = b(i, 0.51); + Q.push(a(P, o), a(P, s), l(P, s), l(P, o)); + } + const r = R(se(e[e.length - 1].vector)); + if (T || (g && e.length === 1)) N.push(k); + else if (q) { + const i = ee(k, r, m); + for (let o = 1 / 29, s = o; s < 1; s += o) N.push(L(i, k, V * 3 * s)); + } else + N.push(l(k, b(r, m)), l(k, b(r, m * 0.99)), a(k, b(r, m * 0.99)), a(k, b(r, m))); + } + return _.concat(N, M.reverse(), Q); +} +function me(e, t = {}) { + var q; + const {streamline: u = 0.5, size: x = 16, last: h = !1} = t; + if (e.length === 0) return []; + let y = 0.15 + (1 - u) * 0.85, + n = Array.isArray(e[0]) ? e : e.map(({x: c, y: p, pressure: g = 0.5}) => [c, p, g]); + if (n.length === 2) { + const c = n[1]; + n = n.slice(0, -1); + for (let p = 1; p < 5; p++) n.push(K(n[0], c, p / 4)); + } + n.length === 1 && (n = [...n, [...l(n[0], [1, 1]), ...n[0].slice(2)]]); + let f = [ + { + point: [n[0][0], n[0][1]], + pressure: n[0][2] >= 0 ? n[0][2] : 0.25, + vector: [1, 1], + distance: 0, + runningLength: 0, + }, + ], + d = !1, + D = 0, + S = f[0], + j = n.length - 1; + for (let c = 1; c < n.length; c++) { + const p = h && c === j ? n[c].slice(0, 2) : K(S.point, n[c], y); + if (ue(S.point, p)) continue; + const g = ie(p, S.point); + if (((D += g), c < j && !d)) { + if (D < x) continue; + d = !0; + } + (S = { + point: p, + pressure: n[c][2] >= 0 ? n[c][2] : 0.5, + vector: G(a(S.point, p)), + distance: g, + runningLength: D, + }), + f.push(S); + } + return (f[0].vector = ((q = f[1]) == null ? void 0 : q.vector) || [0, 0]), f; +} +function ae(e, t = {}) { + return ce(me(e, t), t); +} +var _e = ae; +export { + _e as default, + ae as getStroke, + ce as getStrokeOutlinePoints, + me as getStrokePoints, +}; From 8b4c57b5d12e1baac235731d210e70751b6e1ffd Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 18 Dec 2023 21:19:40 +0100 Subject: [PATCH 2/3] [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 + + + + + +
From cbf331b7b1702a5993cf36c480dde770e21f5bf1 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 20 Dec 2023 13:50:32 +0100 Subject: [PATCH 3/3] [IMP] sign_biometric_oca: Encrypt data --- sign_biometric_oca/models/sign_oca_request.py | 4 ++-- .../static/src/components/biometric.esm.js | 5 +++-- .../src/components/biometric_signature_dialog.xml | 12 ++---------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/sign_biometric_oca/models/sign_oca_request.py b/sign_biometric_oca/models/sign_oca_request.py index c635a150..5abc282c 100644 --- a/sign_biometric_oca/models/sign_oca_request.py +++ b/sign_biometric_oca/models/sign_oca_request.py @@ -18,9 +18,9 @@ class SignOcaRequestSigner(models.Model): def _get_pdf_page_biometric(self, item, box): packet = BytesIO() can = canvas.Canvas(packet, pagesize=(box.getWidth(), box.getHeight())) - if not item.get("value") or not item["value"].get("svg"): + if not item.get("value"): return False - drawing = svg2rlg(BytesIO(b64decode(item["value"]["svg"]))) + drawing = svg2rlg(BytesIO(b64decode(item["value"]))) scaling_x = item["width"] / 100 * float(box.getWidth()) / drawing.width scaling_y = item["height"] / 100 * float(box.getHeight()) / drawing.height diff --git a/sign_biometric_oca/static/src/components/biometric.esm.js b/sign_biometric_oca/static/src/components/biometric.esm.js index cc8df31c..c97d2ad6 100644 --- a/sign_biometric_oca/static/src/components/biometric.esm.js +++ b/sign_biometric_oca/static/src/components/biometric.esm.js @@ -6,8 +6,9 @@ import {registry} from "@web/core/registry"; const signatureSignOca = { uploadSignature: function (parent, item, signatureItem, data) { - item.value = data; - console.log(item); + item.value = data.svg; + // TODO: Transform this in something more standard.... + parent.sensitiveData[item.id] = data.paths; parent.postIframeField(item); parent.checkFilledAll(); var next_items = _.filter( diff --git a/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml b/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml index db30e979..a71209dc 100644 --- a/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml +++ b/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml @@ -40,18 +40,10 @@ - -