From a309ba17f82315ede38b20c242a8fc83f3d2c2cd Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Fri, 12 Mar 2021 14:02:30 +0100 Subject: [PATCH 01/18] Add vault and vault_share module Fix website in manifest Run pre-commit Move models to own files. Apply OCA conventions Add application icon --- vault_share/README.rst | 78 ++++ vault_share/__init__.py | 4 + vault_share/__manifest__.py | 24 + vault_share/controllers/__init__.py | 4 + vault_share/controllers/main.py | 34 ++ vault_share/data/ir_cron.xml | 13 + vault_share/i18n/vault_share.pot | 208 +++++++++ vault_share/models/__init__.py | 4 + vault_share/models/vault_share.py | 72 +++ vault_share/readme/CONTRIBUTORS.rst | 1 + vault_share/readme/DESCRIPTION.rst | 6 + vault_share/security/ir.model.access.csv | 2 + vault_share/security/ir_rule.xml | 12 + vault_share/static/description/icon.png | Bin 0 -> 2287 bytes vault_share/static/description/index.html | 412 ++++++++++++++++++ vault_share/static/src/js/vault_fields.js | 55 +++ vault_share/static/src/js/vault_share.js | 56 +++ .../static/src/js/vault_share_widget.js | 355 +++++++++++++++ vault_share/static/src/js/vault_utils.js | 19 + vault_share/static/src/scss/vault_share.scss | 3 + vault_share/static/src/xml/templates.xml | 48 ++ vault_share/tests/__init__.py | 4 + vault_share/tests/test_share.py | 44 ++ vault_share/views/assets.xml | 32 ++ vault_share/views/menuitems.xml | 16 + vault_share/views/templates.xml | 44 ++ vault_share/views/vault_share_views.xml | 39 ++ 27 files changed, 1589 insertions(+) create mode 100644 vault_share/README.rst create mode 100644 vault_share/__init__.py create mode 100644 vault_share/__manifest__.py create mode 100644 vault_share/controllers/__init__.py create mode 100644 vault_share/controllers/main.py create mode 100644 vault_share/data/ir_cron.xml create mode 100644 vault_share/i18n/vault_share.pot create mode 100644 vault_share/models/__init__.py create mode 100644 vault_share/models/vault_share.py create mode 100644 vault_share/readme/CONTRIBUTORS.rst create mode 100644 vault_share/readme/DESCRIPTION.rst create mode 100644 vault_share/security/ir.model.access.csv create mode 100644 vault_share/security/ir_rule.xml create mode 100644 vault_share/static/description/icon.png create mode 100644 vault_share/static/description/index.html create mode 100644 vault_share/static/src/js/vault_fields.js create mode 100644 vault_share/static/src/js/vault_share.js create mode 100644 vault_share/static/src/js/vault_share_widget.js create mode 100644 vault_share/static/src/js/vault_utils.js create mode 100644 vault_share/static/src/scss/vault_share.scss create mode 100644 vault_share/static/src/xml/templates.xml create mode 100644 vault_share/tests/__init__.py create mode 100644 vault_share/tests/test_share.py create mode 100644 vault_share/views/assets.xml create mode 100644 vault_share/views/menuitems.xml create mode 100644 vault_share/views/templates.xml create mode 100644 vault_share/views/vault_share_views.xml diff --git a/vault_share/README.rst b/vault_share/README.rst new file mode 100644 index 0000000000..79c29aa762 --- /dev/null +++ b/vault_share/README.rst @@ -0,0 +1,78 @@ +============= +Vault - Share +============= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/14.0/vault_share + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-14-0/server-auth-14-0-vault_share + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/251/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module implements possibilities to share specific secrets with external users. This bases on the vault implementation and the generated RSA key pair. + +Share +===== + +This allows an user to share a secret with external users. A share can be generated from a vault entry or directly created by an user. The secret is symmetrically encrypted by a key derived from a pin. To grant access the user has to transmit the link and pin with the external. If either the access counter reaches 0 or the share expires it will be deleted automatically. Due to the usage of a numeric pin and the browser side decryption a share is vulnerable to brute-force attacks and shouldn't be used as a permanent storage for secrets. For long time uses the user should create an account and a vault should be used. + +**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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* initOS GmbH + +Contributors +~~~~~~~~~~~~ + +* Florian Kantelberg + +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/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vault_share/__init__.py b/vault_share/__init__.py new file mode 100644 index 0000000000..05ae53c8cb --- /dev/null +++ b/vault_share/__init__.py @@ -0,0 +1,4 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import controllers, models diff --git a/vault_share/__manifest__.py b/vault_share/__manifest__.py new file mode 100644 index 0000000000..cdbf549b35 --- /dev/null +++ b/vault_share/__manifest__.py @@ -0,0 +1,24 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Vault - Share", + "summary": "Implementation of a mechanism to share secrets", + "license": "AGPL-3", + "version": "14.0.1.0.0", + "website": "https://github.com/OCA/server-auth", + "application": True, + "author": "initOS GmbH, Odoo Community Association (OCA)", + "category": "Vault", + "depends": ["vault"], + "data": [ + "data/ir_cron.xml", + "security/ir.model.access.csv", + "security/ir_rule.xml", + "views/assets.xml", + "views/menuitems.xml", + "views/templates.xml", + "views/vault_share_views.xml", + ], + "qweb": ["static/src/xml/templates.xml"], +} diff --git a/vault_share/controllers/__init__.py b/vault_share/controllers/__init__.py new file mode 100644 index 0000000000..aabfa83edd --- /dev/null +++ b/vault_share/controllers/__init__.py @@ -0,0 +1,4 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import main diff --git a/vault_share/controllers/main.py b/vault_share/controllers/main.py new file mode 100644 index 0000000000..4d908f0669 --- /dev/null +++ b/vault_share/controllers/main.py @@ -0,0 +1,34 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class Controller(http.Controller): + @http.route("/vault/share/", type="http", auth="public") + def vault_share(self, token): + ctx = {"disable_footer": True, "token": token} + secret = request.env["vault.share"].sudo().get(token) + if secret is None: + ctx["error"] = _("The secret expired") + return request.render("vault_share.share", ctx) + + if len(secret) != 1: + ctx["error"] = _("Invalid token") + return request.render("vault_share.share", ctx) + + ctx.update( + { + "encrypted": secret.secret, + "salt": secret.salt, + "iv": secret.iv, + "encrypted_file": secret.secret_file, + "filename": secret.filename, + } + ) + return request.render("vault_share.share", ctx) diff --git a/vault_share/data/ir_cron.xml b/vault_share/data/ir_cron.xml new file mode 100644 index 0000000000..362e5c1905 --- /dev/null +++ b/vault_share/data/ir_cron.xml @@ -0,0 +1,13 @@ + + + + Clean outgoing share + + code + model.clean() + 1 + minutes + -1 + + + diff --git a/vault_share/i18n/vault_share.pot b/vault_share/i18n/vault_share.pot new file mode 100644 index 0000000000..9ab807534b --- /dev/null +++ b/vault_share/i18n/vault_share.pot @@ -0,0 +1,208 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * vault_share +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__accesses +msgid "Access counter" +msgstr "" + +#. module: vault_share +#: model:ir.actions.server,name:vault_share.cron_share_clean_ir_actions_server +#: model:ir.cron,cron_name:vault_share.cron_share_clean +#: model:ir.cron,name:vault_share.cron_share_clean +msgid "Clean outgoing share" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__create_uid +msgid "Created by" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__create_date +msgid "Created on" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__display_name +msgid "Display Name" +msgstr "" + +#. module: vault_share +#: model_terms:ir.ui.view,arch_db:vault_share.share +msgid "Enter the pin:" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__expiration +msgid "Expiration" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__filename +msgid "Filename" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__id +msgid "ID" +msgstr "" + +#. module: vault_share +#: code:addons/vault_share/controllers/main.py:0 +#, python-format +msgid "Invalid token" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__iv +msgid "Iv" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share____last_update +msgid "Last Modified on" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__write_date +msgid "Last Updated on" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__name +msgid "Name" +msgstr "" + +#. module: vault_share +#: code:addons/vault_share/models/vault_share.py:0 +#: model:ir.model.constraint,message:vault_share.constraint_vault_share_value_check +#, python-format +msgid "No value found" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__pin +msgid "Pin" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__salt +msgid "Salt" +msgstr "" + +#. module: vault_share +#. openerp-web +#: code:addons/vault_share/static/src/xml/templates.xml:0 +#: code:addons/vault_share/static/src/xml/templates.xml:0 +#: code:addons/vault_share/static/src/xml/templates.xml:0 +#: code:addons/vault_share/static/src/xml/templates.xml:0 +#, python-format +msgid "Save in a vault" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__secret +msgid "Secret" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__secret_file +msgid "Secret File" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__share_link +msgid "Share URL" +msgstr "" + +#. module: vault_share +#. openerp-web +#: code:addons/vault_share/static/src/js/vault_fields.js:0 +#, python-format +msgid "Share the secret" +msgstr "" + +#. module: vault_share +#. openerp-web +#: code:addons/vault_share/static/src/xml/templates.xml:0 +#: code:addons/vault_share/static/src/xml/templates.xml:0 +#, python-format +msgid "Share the secret with an external user" +msgstr "" + +#. module: vault_share +#: model_terms:ir.ui.view,arch_db:vault_share.share +msgid "Shared file:" +msgstr "" + +#. module: vault_share +#: model_terms:ir.ui.view,arch_db:vault_share.share +msgid "Shared secret:" +msgstr "" + +#. module: vault_share +#: model:ir.actions.act_window,name:vault_share.action_vault_share +#: model:ir.ui.menu,name:vault_share.menu_vault_share +msgid "Shares" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,help:vault_share.field_vault_share__expiration +msgid "Specifies how long a share can be accessed until deletion." +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,help:vault_share.field_vault_share__accesses +msgid "Specifies how often a share can be accessed before deletion." +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,help:vault_share.field_vault_share__pin +msgid "The pin needed to decrypt the share." +msgstr "" + +#. module: vault_share +#: code:addons/vault_share/controllers/main.py:0 +#, python-format +msgid "The secret expired" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__token +msgid "Token" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,field_description:vault_share.field_vault_share__user_id +msgid "User" +msgstr "" + +#. module: vault_share +#: model:ir.model.fields,help:vault_share.field_vault_share__share_link +msgid "Using this link and pin people can access the secret." +msgstr "" + +#. module: vault_share +#: code:addons/vault_share/models/vault_share.py:0 +#: model:ir.model,name:vault_share.model_vault_share +#, python-format +msgid "Vault share outgoing secrets" +msgstr "" diff --git a/vault_share/models/__init__.py b/vault_share/models/__init__.py new file mode 100644 index 0000000000..51a08ee32d --- /dev/null +++ b/vault_share/models/__init__.py @@ -0,0 +1,4 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import vault_share diff --git a/vault_share/models/vault_share.py b/vault_share/models/vault_share.py new file mode 100644 index 0000000000..24da5c41a9 --- /dev/null +++ b/vault_share/models/vault_share.py @@ -0,0 +1,72 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import datetime, timedelta +from uuid import uuid4 + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class VaultShare(models.Model): + _name = "vault.share" + _description = _("Vault share outgoing secrets") + + user_id = fields.Many2one("res.users", default=lambda self: self.env.uid) + name = fields.Char(required=True) + share_link = fields.Char( + "Share URL", + compute="_compute_url", + store=False, + help="Using this link and pin people can access the secret.", + ) + token = fields.Char(readonly=True, required=True, default=lambda self: uuid4()) + secret = fields.Char() + secret_file = fields.Char() + filename = fields.Char() + salt = fields.Char(required=True) + iv = fields.Char(required=True) + pin = fields.Char(required=True, help="The pin needed to decrypt the share.") + accesses = fields.Integer( + "Access counter", + default=5, + help="Specifies how often a share can be accessed before deletion.", + ) + expiration = fields.Datetime( + default=lambda self: datetime.now() + timedelta(days=7), + help="Specifies how long a share can be accessed until deletion.", + ) + + _sql_constraints = [ + ( + "value_check", + "CHECK(secret IS NOT NULL OR secret_file IS NOT NULL)", + _("No value found"), + ), + ] + + @api.depends("token") + def _compute_url(self): + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + for rec in self: + rec.share_link = f"{base_url}/vault/share/{rec.token}" + + @api.model + def get(self, token): + rec = self.search([("token", "=", token)], limit=1) + if not rec: + return rec + + if datetime.now() < rec.expiration and rec.accesses > 0: + rec.accesses -= 1 + return rec + + rec.unlink() + return None + + @api.model + def clean(self): + now = datetime.now() + self.search([("expiration", "<=", now), ("accesses", "<=", 0)]).unlink() diff --git a/vault_share/readme/CONTRIBUTORS.rst b/vault_share/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..e202826121 --- /dev/null +++ b/vault_share/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Florian Kantelberg diff --git a/vault_share/readme/DESCRIPTION.rst b/vault_share/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..51e526ca52 --- /dev/null +++ b/vault_share/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module implements possibilities to share specific secrets with external users. This bases on the vault implementation and the generated RSA key pair. + +Share +===== + +This allows an user to share a secret with external users. A share can be generated from a vault entry or directly created by an user. The secret is symmetrically encrypted by a key derived from a pin. To grant access the user has to transmit the link and pin with the external. If either the access counter reaches 0 or the share expires it will be deleted automatically. Due to the usage of a numeric pin and the browser side decryption a share is vulnerable to brute-force attacks and shouldn't be used as a permanent storage for secrets. For long time uses the user should create an account and a vault should be used. diff --git a/vault_share/security/ir.model.access.csv b/vault_share/security/ir.model.access.csv new file mode 100644 index 0000000000..46bc795e07 --- /dev/null +++ b/vault_share/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_vault_share,access_vault_share,model_vault_share,base.group_user,1,1,1,1 diff --git a/vault_share/security/ir_rule.xml b/vault_share/security/ir_rule.xml new file mode 100644 index 0000000000..936aba6b94 --- /dev/null +++ b/vault_share/security/ir_rule.xml @@ -0,0 +1,12 @@ + + + + vault.share.access.owner + + [('user_id', '=', user.id)] + + + + + + diff --git a/vault_share/static/description/icon.png b/vault_share/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c9035d9b2247bbe8008a1601aafb0524a2c13d GIT binary patch literal 2287 zcmVu1JM?9g1fDd5-sLv%rO|#VvfNWiDip926JK= z(_)Um4v=awN51v&u-B{S_cL>B2{A3^n9NZ*ro|kCIkrNO7IOk{;;K@MIWos+2)CG{ zn3D|<)Am(_27=meiONxl(UW&{JLqy@yB4CN>1A>}uwyU7TX(UmyOXX=7Th9BYjfPV zd!2Xkvn*AFniO9TwVxF0ha4xY7x72h$qLkr>p~`Tf~oUnHnKUT>G@NqF~Zmh6!JOb zbY{z54juR?Pwp9Hpq=<@6e#DpJad-cT|CdfN)^qI80J{?k?kr%s0Uf10m6e2{@m6+q&y@*L;RpXAq9?@|o>_eW}c zC(u?-M%QNQImh14#1miR#CSqb0NVO^bo>a%$M(^eLe8;4)Rx_wUafi=KXjNQgT1sR z#6(?Lc8`3PZ;TDmU3HxLs+Ib>W^2YVBM;7~5Dub&{I4+rZ2xv4m|it9=rJ)&Xk%{ zt?V#T9)f8y$7cV*5A%U)l_z|y{DYsL{v*>%j>ibMz_lwUd3pNZly$kb5uO@-fNYcc zoS6NXlyi=~kBD*)zpb|IIlxe)#OLVm$<|mE z*Jq9`0>~hJ=49y3b+8D4y3Fs96Bd0(!lDc@FEA0gGj z##J2&S`(^Oo1IMt4A9g6364GUF&0-=aN2V8wXb8t&dPWQ`J1d-d0*FZ#?xm`g+jTQ z7#Q!$(v$059~8hT7Jc^`Vvejz6;HI{8%Zzc+*w#L%WP9Ghq=OnS!HVRH|F=Yfz&22 zr!YIi?bLv|!{xlI7|;N7LVZly%(2-$uqRPfN^+I+?{cvaUuBXq#}3MO>deWo`@m5q zA0J8ym?D?2OmoNYbM(0CU`~~j$YwLgX87O{CMVJY1VEj~)W{rLVo7?; z$uRuzQI1axrv?h8Yn=Ss+uV%nkn3cQy*`4CGRJ24p`%QGXqYXjAOg5~PEWnYsRdW{ zQ%yL1$a)BZ!5oRW+B1#r{RT~&GN76&ED#-xj6KWbV=02NdX3-z)xd#WPlOhnzMj4!39CinO%5n>=ArvYQxrp=t7j|sL0f_p8U zt=H!a?0A1e%cqvFa$r;c%EFedA3AfqxBha9VUA}7Pcf^$jq15d%LF?#LGF^nRow~n zgOrX-f0UZf9F-Q(!Lc^(kc}NY->GCdNBLIERW8huF9EKDQ<`P!>>2X9J&4*5Er-p> zIoV(>`bMZD=Onmpo&P1@o1P8E@h8~KCDE$4**Pb=gI8D1NdnEiIxA0>b8KUc1Eok4$&Yn*1tS3%SI4myaS#-@@;@=`UC-4I`<9f~^oJ%k}WdAuPu3G-b zQ~PTNJv&#-tX)gBD$R3iHk$bKX3%#wT6Me^ssEKlf@)<4$Qx_3k(?9mb5w_{Eqawp zg2ny6m2{8=B)oek@qumY=6dE%rTj>!Aa!(%N)DnI_9@Zi#b#r7uzkCYnuseM*rWL<181k zt!2lDs>K{>$NA5yXX8^Zeo^%8+Ch6)2X-c_4eAFQdJdusZz1U0gE4w4CJiJ_4;@?u zlA#y3ryJLk-vVMSHJsHW&R73&x`q`c}k@c7=tSF`bk~PYt5OE zlMow>8YlE<1Se-QPB3i0id|z8 + + + + + +Vault - Share + + + +
+

Vault - Share

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runbot

+

This module implements possibilities to share specific secrets with external users. This bases on the vault implementation and the generated RSA key pair.

+
+

Share

+

This allows an user to share a secret with external users. A share can be generated from a vault entry or directly created by an user. The secret is symmetrically encrypted by a key derived from a pin. To grant access the user has to transmit the link and pin with the external. If either the access counter reaches 0 or the share expires it will be deleted automatically. Due to the usage of a numeric pin and the browser side decryption a share is vulnerable to brute-force attacks and shouldn’t be used as a permanent storage for secrets. For long time uses the user should create an account and a vault should be used.

+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • initOS GmbH
  • +
+
+
+

Contributors

+ +
+
+

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/server-auth project on GitHub.

+

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

+
+
+
+ + diff --git a/vault_share/static/src/js/vault_fields.js b/vault_share/static/src/js/vault_fields.js new file mode 100644 index 0000000000..13ee282ba7 --- /dev/null +++ b/vault_share/static/src/js/vault_fields.js @@ -0,0 +1,55 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +odoo.define("vault.share.fields", function (require) { + "use strict"; + + var core = require("web.core"); + var sh_utils = require("vault.share.utils"); + var utils = require("vault.utils"); + var vault = require("vault"); + var vault_fields = require("vault.fields"); + + var _t = core._t; + + // Extend the widget to share + vault_fields.VaultField.include({ + events: _.extend(vault_fields.VaultField.prototype.events, { + "click .o_vault_share": "_onShareValue", + }), + + /** + * Share the value for an external user + * + * @private + * @param {OdooEvent} ev + */ + _onShareValue: async function (ev) { + ev.stopPropagation(); + + const iv = await utils.generate_iv_base64(); + const pin = sh_utils.generate_pin(sh_utils.PinSize); + const salt = utils.generate_bytes(utils.SaltLength).buffer; + const key = await utils.derive_key(pin, salt, 4000); + const public_key = await vault.get_public_key(); + const value = await this._decrypt(this.value); + + this.do_action({ + type: "ir.actions.act_window", + title: _t("Share the secret"), + target: "new", + res_model: "vault.share", + views: [[false, "form"]], + context: { + default_secret: await utils.sym_encrypt(key, value, iv), + default_pin: await utils.asym_encrypt( + public_key, + pin + utils.generate_iv_base64() + ), + default_iv: iv, + default_salt: utils.toBase64(salt), + }, + }); + }, + }); +}); diff --git a/vault_share/static/src/js/vault_share.js b/vault_share/static/src/js/vault_share.js new file mode 100644 index 0000000000..2af6783193 --- /dev/null +++ b/vault_share/static/src/js/vault_share.js @@ -0,0 +1,56 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +odoo.define("vault.share", function (require) { + "use strict"; + + require("web.dom_ready"); + + var utils = require("vault.utils"); + var data = {}; + + // Find the elements in the document and check if they have values + function find_elements() { + for (const id of ["encrypted", "salt", "iv", "encrypted_file", "filename"]) + if (!data[id]) { + const element = document.getElementById(id); + data[id] = element && element.value; + } + } + + document.getElementById("pin").onchange = async function () { + find_elements(); + + // Derive the key from the pin + const key = await utils.derive_key( + this.value, + utils.fromBase64(data.salt), + 4000 + ); + + const secret = document.getElementById("secret"); + const secret_file = document.getElementById("secret_file"); + if (!secret || !secret_file) return; + + // There is no secret to decrypt + if (!this.value) { + secret.setAttribute("class", "alert alert-danger col-12"); + secret_file.setAttribute("class", "alert alert-danger col-12"); + return; + } + + // Decrypt the data and show the value + if (data.encrypted) { + secret.value = await utils.sym_decrypt(key, data.encrypted, data.iv); + secret.setAttribute("class", "alert alert-success col-12"); + } + + if (data.encrypted_file) { + const content = await utils.sym_decrypt(key, data.encrypted_file, data.iv); + const file = new File([atob(content)], data.filename); + secret_file.text = data.filename; + secret_file.setAttribute("href", window.URL.createObjectURL(file)); + secret_file.setAttribute("class", "alert alert-success col-12"); + } + }; +}); diff --git a/vault_share/static/src/js/vault_share_widget.js b/vault_share/static/src/js/vault_share_widget.js new file mode 100644 index 0000000000..6b5c287c24 --- /dev/null +++ b/vault_share/static/src/js/vault_share_widget.js @@ -0,0 +1,355 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +odoo.define("vault.share.widget", function (require) { + "use strict"; + + var basic_fields = require("web.basic_fields"); + var core = require("web.core"); + var registry = require("web.field_registry"); + var sh_utils = require("vault.share.utils"); + var utils = require("vault.utils"); + var vault = require("vault"); + var vault_fields = require("vault.fields"); + + var QWeb = core.qweb; + + // Widget used to view the encrypted pin + var VaultPinField = basic_fields.InputField.extend(vault_fields.VaultAbstract, { + supportedFieldTypes: ["char"], + events: _.extend({}, basic_fields.InputField.prototype.events, { + "click .o_vault_show": "_onShowValue", + "click .o_vault_clipboard": "_onCopyValue", + }), + template: "FieldPinVault", + + /** + * Prepare the widget by evaluating the field attributes and setting the defaults + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + + this.pin_size = this.attrs.pin_size || sh_utils.PinSize; + }, + + /** + * Decrypt the value using the private key of the vault and slice it to + * the actual pin size because there is a salt following + * + * @private + * @param {String} data + * @returns the decrypted data + */ + _decrypt: async function (data) { + if (!data) return data; + + const private_key = await vault.get_private_key(); + const plain = await utils.asym_decrypt(private_key, data); + return plain.slice(0, this.pin_size); + }, + + /** + * Render the decrypted value or the stars + * + * @private + */ + _renderReadonly: function () { + this._renderValue(this.decrypted_value || "********"); + }, + + /** + * Render the decrypted value or the stars + * + * @private + */ + _renderEdit: function () { + this._renderValue(this.decrypted_value || "********"); + }, + + /** + * Render the field using the template + * + * @private + * @param {String} value + */ + _renderValue: function (value) { + this.$el.html( + QWeb.render(this.template, { + widget: self, + value: value, + show: !this.decrypted, + }) + ); + }, + }); + + // Widget used to create shared outgoing secrets encrypted with a pin + var VaultShareField = vault_fields.VaultField.extend(vault_fields.VaultAbstract, { + events: _.extend({}, vault_fields.VaultField.prototype.events, { + "click .o_vault_save": "_onSaveValue", + }), + template: "FieldVaultShare", + + /** + * Prepare the widget by evaluating the field attributes and setting the defaults + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + + this.pin_size = this.attrs.pin_size || sh_utils.PinSize; + this.field_salt = this.attrs.salt || "salt"; + this.field_pin = this.attrs.pin || "pin"; + }, + + /** + * Encrypt the pin with a random salt to make it hard to guess him by + * encrypting every possilbilty with the public key. Store the pin in the + * proper field + * + * @private + */ + _storePin: async function () { + const salt = utils.generate_iv_base64(); + const crypted_pin = await utils.asym_encrypt( + await vault.get_public_key(), + this.pin + salt + ); + this._setFieldValue(this.field_pin, crypted_pin); + }, + + /** + * Returns the pin from the class, record data, or generate a new pin if + * none s currently available + * + * @private + * @returns the pin + */ + _getPin: async function () { + if (this.pin) return this.pin; + + this.pin = this.recordData[this.field_pin]; + if (this.pin) { + // Decrypt the pin and slice him to the configured pin size + const private_key = await vault.get_private_key(); + const plain = await utils.asym_decrypt(private_key, this.pin); + this.pin = plain.slice(0, this.pin_size); + return this.pin; + } + + // Generate a new pin and store it + this.pin = sh_utils.generate_pin(this.pin_size); + await this._storePin(); + return this.pin; + }, + + /** + * Returns the salt from the class, record data, or generate a new salt if + * none is currently available + * + * @private + * @returns the salt + */ + _getSalt: function () { + if (this.salt) return this.salt; + + this.salt = this.recordData[this.field_salt]; + if (this.salt) return this.salt; + + // Generate a new salt and store him + this.salt = utils.toBase64(utils.generate_bytes(utils.SaltLength).buffer); + this._setFieldValue(this.field_salt, this.salt); + return this.salt; + }, + + /** + * Decrypt the encrypted data using the pin, IV and salt + * + * @private + * @param {String} crypted + */ + _decrypt: async function (crypted) { + const iv = this._getIV(); + const pin = await this._getPin(); + const salt = utils.fromBase64(this._getSalt()); + + const key = await utils.derive_key(pin, salt, 4000); + return await utils.sym_decrypt(key, crypted, iv); + }, + + /** + * Encrypt the data using the pin, IV and salt + * + * @private + * @param {String} data + */ + _encrypt: async function (data) { + const iv = this._getIV(); + const pin = await this._getPin(); + const salt = utils.fromBase64(this._getSalt()); + + const key = await utils.derive_key(pin, salt, 4000); + return await utils.sym_encrypt(key, data, iv); + }, + + /** + * Resets the content to the formated value in readonly mode. + * + * @override + * @private + */ + _renderReadonly: function () { + this.$el.html( + QWeb.render(this.template, { + widget: self, + value: this.decrypted_value || "********", + show: !this.decrypted, + }) + ); + }, + + /** + * @override + * @returns {String} the content of the input + */ + _getValue: function () { + return this.$input.val(); + }, + }); + + // Widget used to view shared incoming secrets encrypted with public keys + var VaultShareFile = vault_fields.VaultFile.extend(vault_fields.VaultAbstract, { + store_model: "vault.file", + template: "FileVaultShare", + events: _.extend({}, vault_fields.VaultFile.prototype.events, { + "click .o_vault_save": "_onSaveValue", + }), + + /** + * Prepare the widget by evaluating the field attributes and setting the defaults + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + + this.field_key = this.attrs.key || "key"; + this.pin_size = this.attrs.pin_size || sh_utils.PinSize; + this.field_salt = this.attrs.salt || "salt"; + this.field_pin = this.attrs.pin || "pin"; + }, + + /** + * Encrypt the pin with a random salt to make it hard to guess him by + * encrypting every possilbilty with the public key. Store the pin in the + * proper field + * + * @private + */ + _storePin: async function () { + const salt = utils.generate_iv_base64(); + const crypted_pin = await utils.asym_encrypt( + await vault.get_public_key(), + this.pin + salt + ); + this._setFieldValue(this.field_pin, crypted_pin); + }, + + /** + * Returns the pin from the class, record data, or generate a new pin if + * none s currently available + * + * @private + * @returns the pin + */ + _getPin: async function () { + if (this.pin) return this.pin; + + this.pin = this.recordData[this.field_pin]; + if (this.pin) { + // Decrypt the pin and slice him to the configured pin size + const private_key = await vault.get_private_key(); + const plain = await utils.asym_decrypt(private_key, this.pin); + this.pin = plain.slice(0, this.pin_size); + return this.pin; + } + + // Generate a new pin and store it + this.pin = sh_utils.generate_pin(this.pin_size); + await this._storePin(); + return this.pin; + }, + + /** + * Returns the salt from the class, record data, or generate a new salt if + * none is currently available + * + * @private + * @returns the salt + */ + _getSalt: function () { + if (this.salt) return this.salt; + + this.salt = this.recordData[this.field_salt]; + if (this.salt) return this.salt; + + // Generate a new salt and store him + this.salt = utils.toBase64(utils.generate_bytes(utils.SaltLength).buffer); + this._setFieldValue(this.field_salt, this.salt); + return this.salt; + }, + + _renderReadonly: function () { + this.do_toggle(Boolean(this.value)); + if (this.value) { + this.$el.html( + QWeb.render(this.template, { + widget: this, + filename: this.filename_value, + }) + ); + + const $el = this.$(".link"); + if (this.recordData.id) $el.css("cursor", "pointer"); + else $el.css("cursor", "not-allowed"); + } + }, + + /** + * Decrypt the encrypted data using the pin, IV and salt + * + * @private + * @param {String} crypted + */ + _decrypt: async function (crypted) { + const iv = this._getIV(); + const pin = await this._getPin(); + const salt = utils.fromBase64(this._getSalt()); + + const key = await utils.derive_key(pin, salt, 4000); + return await utils.sym_decrypt(key, crypted, iv); + }, + + /** + * Encrypt the data using the pin, IV and salt + * + * @private + * @param {String} data + */ + _encrypt: async function (data) { + const iv = this._getIV(); + const pin = await this._getPin(); + const salt = utils.fromBase64(this._getSalt()); + + const key = await utils.derive_key(pin, salt, 4000); + return await utils.sym_encrypt(key, data, iv); + }, + }); + + registry.add("vault_pin", VaultPinField); + registry.add("vault_share", VaultShareField); + registry.add("vault_share_file", VaultShareFile); +}); diff --git a/vault_share/static/src/js/vault_utils.js b/vault_share/static/src/js/vault_utils.js new file mode 100644 index 0000000000..c85a522f3d --- /dev/null +++ b/vault_share/static/src/js/vault_utils.js @@ -0,0 +1,19 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +odoo.define("vault.share.utils", function (require) { + "use strict"; + + const PinSize = 5; + + var utils = require("vault.utils"); + + function generate_pin(pin_size) { + return utils.generate_secret(pin_size, "0123456789"); + } + + return { + PinSize: PinSize, + generate_pin: generate_pin, + }; +}); diff --git a/vault_share/static/src/scss/vault_share.scss b/vault_share/static/src/scss/vault_share.scss new file mode 100644 index 0000000000..09e4e6b160 --- /dev/null +++ b/vault_share/static/src/scss/vault_share.scss @@ -0,0 +1,3 @@ +.o_vault_share { + white-space: pre-wrap; +} diff --git a/vault_share/static/src/xml/templates.xml b/vault_share/static/src/xml/templates.xml new file mode 100644 index 0000000000..0f6f2334a6 --- /dev/null +++ b/vault_share/static/src/xml/templates.xml @@ -0,0 +1,48 @@ + + + + +
+ +
+
+ + + +
+
+
+ + +
+ + +
+
+ + + +