From c0bffd15a1d26927d61055f3388bc3fd6a86381a Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Fri, 12 Mar 2021 14:02:30 +0100 Subject: [PATCH 01/71] Add vault and vault_share module Fix website in manifest Run pre-commit Move models to own files. Apply OCA conventions Remove logging causing the tests to fail Add application icon Split models into files. Use lambda for defaults Add icon to menuitem Use lambda as suggested Add missing keepass lib Show information if user lacks public key Add note about no https Fix domain for user_id in send wizard. Add permanent note to wizard --- vault/TECHNICAL.rst | 143 +++++ vault/__init__.py | 4 + vault/__manifest__.py | 34 ++ vault/controllers/__init__.py | 4 + vault/controllers/main.py | 104 ++++ vault/models/__init__.py | 17 + vault/models/abstract_vault.py | 67 ++ vault/models/abstract_vault_field.py | 56 ++ vault/models/res_users.py | 64 ++ vault/models/res_users_key.py | 75 +++ vault/models/vault.py | 158 +++++ vault/models/vault_entry.py | 132 ++++ vault/models/vault_field.py | 17 + vault/models/vault_file.py | 17 + vault/models/vault_inbox.py | 92 +++ vault/models/vault_log.py | 46 ++ vault/models/vault_right.py | 100 +++ vault/models/vault_tag.py | 16 + vault/readme/CONTRIBUTORS.rst | 1 + vault/readme/DESCRIPTION.rst | 3 + vault/readme/ROADMAP.rst | 8 + vault/security/ir.model.access.csv | 15 + vault/security/ir_rule.xml | 88 +++ vault/static/description/icon.png | Bin 0 -> 2287 bytes vault/static/lib/kdbxweb/kdbxweb.min.js | 2 + vault/static/src/js/user_menu.js | 26 + vault/static/src/js/vault.js | 411 +++++++++++++ vault/static/src/js/vault_controller.js | 350 +++++++++++ vault/static/src/js/vault_export.js | 134 ++++ vault/static/src/js/vault_import.js | 259 ++++++++ vault/static/src/js/vault_inbox.js | 78 +++ vault/static/src/js/vault_utils.js | 577 ++++++++++++++++++ vault/static/src/js/vault_widget.js | 544 +++++++++++++++++ vault/static/src/scss/vault.scss | 7 + vault/static/src/xml/templates.xml | 178 ++++++ vault/static/tests/vault_tests.js | 209 +++++++ vault/tests/__init__.py | 4 + vault/tests/test_inbox.py | 115 ++++ vault/tests/test_log.py | 41 ++ vault/tests/test_rights.py | 128 ++++ vault/tests/test_user.py | 29 + vault/tests/test_vault.py | 131 ++++ vault/tests/test_widgets.py | 156 +++++ vault/views/assets.xml | 37 ++ vault/views/menuitems.xml | 57 ++ vault/views/res_users_views.xml | 71 +++ vault/views/templates.xml | 91 +++ vault/views/vault_entry_views.xml | 150 +++++ vault/views/vault_field_views.xml | 36 ++ vault/views/vault_file_views.xml | 36 ++ vault/views/vault_inbox_views.xml | 42 ++ vault/views/vault_log_views.xml | 18 + vault/views/vault_right_views.xml | 70 +++ vault/views/vault_views.xml | 104 ++++ vault/wizards/__init__.py | 9 + vault/wizards/vault_export_wizard.py | 63 ++ vault/wizards/vault_export_wizard.xml | 29 + vault/wizards/vault_import_wizard.py | 134 ++++ vault/wizards/vault_import_wizard.xml | 53 ++ vault/wizards/vault_send_wizard.py | 51 ++ vault/wizards/vault_send_wizard.xml | 35 ++ vault/wizards/vault_store_wizard.py | 47 ++ vault/wizards/vault_store_wizard.xml | 39 ++ 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/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/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 ++ 87 files changed, 6703 insertions(+) create mode 100644 vault/TECHNICAL.rst create mode 100644 vault/__init__.py create mode 100644 vault/__manifest__.py create mode 100644 vault/controllers/__init__.py create mode 100644 vault/controllers/main.py create mode 100644 vault/models/__init__.py create mode 100644 vault/models/abstract_vault.py create mode 100644 vault/models/abstract_vault_field.py create mode 100644 vault/models/res_users.py create mode 100644 vault/models/res_users_key.py create mode 100644 vault/models/vault.py create mode 100644 vault/models/vault_entry.py create mode 100644 vault/models/vault_field.py create mode 100644 vault/models/vault_file.py create mode 100644 vault/models/vault_inbox.py create mode 100644 vault/models/vault_log.py create mode 100644 vault/models/vault_right.py create mode 100644 vault/models/vault_tag.py create mode 100644 vault/readme/CONTRIBUTORS.rst create mode 100644 vault/readme/DESCRIPTION.rst create mode 100644 vault/readme/ROADMAP.rst create mode 100644 vault/security/ir.model.access.csv create mode 100644 vault/security/ir_rule.xml create mode 100644 vault/static/description/icon.png create mode 100644 vault/static/lib/kdbxweb/kdbxweb.min.js create mode 100644 vault/static/src/js/user_menu.js create mode 100644 vault/static/src/js/vault.js create mode 100644 vault/static/src/js/vault_controller.js create mode 100644 vault/static/src/js/vault_export.js create mode 100644 vault/static/src/js/vault_import.js create mode 100644 vault/static/src/js/vault_inbox.js create mode 100644 vault/static/src/js/vault_utils.js create mode 100644 vault/static/src/js/vault_widget.js create mode 100644 vault/static/src/scss/vault.scss create mode 100644 vault/static/src/xml/templates.xml create mode 100644 vault/static/tests/vault_tests.js create mode 100644 vault/tests/__init__.py create mode 100644 vault/tests/test_inbox.py create mode 100644 vault/tests/test_log.py create mode 100644 vault/tests/test_rights.py create mode 100644 vault/tests/test_user.py create mode 100644 vault/tests/test_vault.py create mode 100644 vault/tests/test_widgets.py create mode 100644 vault/views/assets.xml create mode 100644 vault/views/menuitems.xml create mode 100644 vault/views/res_users_views.xml create mode 100644 vault/views/templates.xml create mode 100644 vault/views/vault_entry_views.xml create mode 100644 vault/views/vault_field_views.xml create mode 100644 vault/views/vault_file_views.xml create mode 100644 vault/views/vault_inbox_views.xml create mode 100644 vault/views/vault_log_views.xml create mode 100644 vault/views/vault_right_views.xml create mode 100644 vault/views/vault_views.xml create mode 100644 vault/wizards/__init__.py create mode 100644 vault/wizards/vault_export_wizard.py create mode 100644 vault/wizards/vault_export_wizard.xml create mode 100644 vault/wizards/vault_import_wizard.py create mode 100644 vault/wizards/vault_import_wizard.xml create mode 100644 vault/wizards/vault_send_wizard.py create mode 100644 vault/wizards/vault_send_wizard.xml create mode 100644 vault/wizards/vault_store_wizard.py create mode 100644 vault/wizards/vault_store_wizard.xml 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/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/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/TECHNICAL.rst b/vault/TECHNICAL.rst new file mode 100644 index 0000000000..4a5456445d --- /dev/null +++ b/vault/TECHNICAL.rst @@ -0,0 +1,143 @@ +:: + + ┌───────┐ ┏━━━━━━━━━━━━━┓ ╔═══════════╗ + │ input │ ┃ unencrypted ┃ ║ encrypted ║ + └───────┘ ┗━━━━━━━━━━━━━┛ ╚═══════════╝ + +Vault +===== + +Each vault stores entries with enrypted fields and files in a tree like structure. The access is controlled per vault. Every added user can read the secrets of a vault. Otherwise the users can receive permission to share the vault with other users, to write secrets in the vault, or to delete entries of the vault. The databases stores the public and password protected private key of each user. The password used for the private key is derived from a password entered by the user and should be different than the password used for the login. Keep in mind that the meta information like field name or file names aren't encrypted. + +Shared-key encryption +===================== + +To be able to securely share sensitive data between all users a shared-key encryption is used. All users share a common secret for each vault. This secret is encrypted by the public key of each user to grant access to the user by using the private key to restore the secret. + +Encryption of master key +------------------------ + +:: + + . ┏━━━━━━━━━━━━┓ + ┃ Master key ┃ + ┗━━━━━━━━━━━━┛ + ┏━━━━━━━━━━━━━━━━━┓ ┃ + ┃ User ┃ ▼ + ┃ ┃ ┏━━━━━━━━━┓ + ┃ ┏━━━━━━━━━━━━━┓ ┃ ┃ encrypt ┃ ╔════════════╗ + ┃ ┃ Public key ┃━━━━▶┃ (RSA) ┃━━━━━▶║ Master key ║ + ┃ ┗━━━━━━━━━━━━━┛ ┃ ┗━━━━━━━━━┛ ╚════════════╝ + ┃ ╔═════════════╗ ┃ + ┃ ║ Private key ║ ┃ + ┃ ╚═════════════╝ ┃ + ┗━━━━━━━━━━━━━━━━━┛ + +Decryption of master key +------------------------ + +:: + + . ┌──────────┐ ┏━━━━━━━━━━┓ + │ Password │━━━━▶┃ derive ┃ + └──────────┘ ┃ (PBKDF2) ┃ + ┗━━━━━━━━━━┛ + ┃ + ┏━━━━━━━━━━━━━━━━━┓ ▼ ╔════════════╗ + ┃ User ┃ ┏━━━━━━━━━━┓ ║ Master key ║ + ┃ ┃ ┃ Password ┃ ╚════════════╝ + ┃ ┏━━━━━━━━━━━━━┓ ┃ ┗━━━━━━━━━━┛ ┃ + ┃ ┃ Public key ┃ ┃ ┃ ▼ + ┃ ┗━━━━━━━━━━━━━┛ ┃ ▼ ┏━━━━━━━━━┓ + ┃ ╔═════════════╗ ┃ ┏━━━━━━━━┓ ┏━━━━━━━━━━━━━┓ ┃ decrypt ┃ ┏━━━━━━━━━━━━┓ + ┃ ║ Private key ║━━━━━┃ unlock ┃━━▶┃ Private key ┃━━━▶┃ (RSA) ┃━━━━━▶┃ Master key ┃ + ┃ ╚═════════════╝ ┃ ┗━━━━━━━━┛ ┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━┛ + ┗━━━━━━━━━━━━━━━━━┛ + +Symmetric encryption of the data +================================ + +The symmetric cipher AES is used with the common master key to encrypt/decrypt the secrets of the vaults. The encryption parameter and encrypted data is stored in the database while everything else happens in the browser. + +Encryption of data +------------------ + +:: + + . ┏━━━━━━━━━━━━┓ + ┃ Master key ┃ + ┗━━━━━━━━━━━━┛ + ┃ ┏━━━━━━━━━━━━━━━━━━┓ + ▼ ┃ Database ┃ + ┏━━━━━━━━━┓ ┃ ┃ + ┏━━━━━━━━━━━━┓ ┃ encrypt ┃ ┃╔════════════════╗┃ + ┃ Plain text ┃━━▶┃ (AES) ┃━━━▶║ Encrypted data ║┃ + ┗━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ ┃╚════════════════╝┃ + ┃ ┃┏━━━━━━━━━━━━━━━━┓┃ + ┗━━━━━━━━▶┃ Parameters ┃┃ + ┃┗━━━━━━━━━━━━━━━━┛┃ + ┗━━━━━━━━━━━━━━━━━━┛ + +Decryption of data +------------------ + +:: + + . ┏━━━━━━━━━━━━┓ + ┃ Master key ┃ + ┗━━━━━━━━━━━━┛ + ┏━━━━━━━━━━━━━━━━━━┓ ┃ + ┃ Database ┃ ▼ + ┃ ┃ ┏━━━━━━━━━┓ + ┃╔════════════════╗┃ ┃ decrypt ┃ ┏━━━━━━━━━━━━┓ + ┃║ Encrypted data ║━━━▶┃ (AES) ┃━━▶┃ Plain text ┃ + ┃╚════════════════╝┃ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━┛ + ┃┏━━━━━━━━━━━━━━━━┓┃ ▲ + ┃┃ Parameters ┃━━━━━━━━┛ + ┃┗━━━━━━━━━━━━━━━━┛┃ + ┗━━━━━━━━━━━━━━━━━━┛ + +Inbox +===== + +This allows an user to receive encrypted secrets by external or internal Odoo users. External users have to use either the owner specific inbox link from his preferences or the link of an already created inbox. The value is symmetrically encrypted. The key for the encryption is wrapped with the public key of the user of the inbox to grant the user the access to the key. Internal users can directly send a secret from a vault entry to another user who has enabled this feature. If a direct link is used the access counter and expiration time can block an overwrite. + +Encryption of inbox +------------------- + +:: + + . ┏━━━━━━━━━━━━┓ + ┃ Plain data ┃ + ┗━━━━━━━━━━━━┛ + ┏━━━━━━━━━━━━━━━━━┓ ┃ + ┃ User ┃ ▼ + ┃ ┃ ┏━━━━━━━━━┓ + ┃ ┏━━━━━━━━━━━━━┓ ┃ ┃ encrypt ┃ ╔════════════════╗ + ┃ ┃ Public key ┃━━━━▶┃ (RSA) ┃━━━━━▶║ Encrypted data ║ + ┃ ┗━━━━━━━━━━━━━┛ ┃ ┗━━━━━━━━━┛ ╚════════════════╝ + ┃ ╔═════════════╗ ┃ + ┃ ║ Private key ║ ┃ + ┃ ╚═════════════╝ ┃ + ┗━━━━━━━━━━━━━━━━━┛ + +Decryption of inbox +------------------- + +:: + + . ┌──────────┐ ┏━━━━━━━━━━┓ + │ Password │━━━━▶┃ derive ┃ + └──────────┘ ┃ (PBKDF2) ┃ + ┗━━━━━━━━━━┛ + ┃ + ┏━━━━━━━━━━━━━━━━━┓ ▼ ╔════════════════╗ + ┃ User ┃ ┏━━━━━━━━━━┓ ║ Encrypted data ║ + ┃ ┃ ┃ Password ┃ ╚════════════════╝ + ┃ ┏━━━━━━━━━━━━━┓ ┃ ┗━━━━━━━━━━┛ ┃ + ┃ ┃ Public key ┃ ┃ ┃ ▼ + ┃ ┗━━━━━━━━━━━━━┛ ┃ ▼ ┏━━━━━━━━━┓ + ┃ ╔═════════════╗ ┃ ┏━━━━━━━━┓ ┏━━━━━━━━━━━━━┓ ┃ decrypt ┃ ┏━━━━━━━━━━━━┓ + ┃ ║ Private key ║━━━━━┃ unlock ┃━━▶┃ Private key ┃━━━▶┃ (RSA) ┃━━━━━▶┃ Plain data ┃ + ┃ ╚═════════════╝ ┃ ┗━━━━━━━━┛ ┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━┛ + ┗━━━━━━━━━━━━━━━━━┛ diff --git a/vault/__init__.py b/vault/__init__.py new file mode 100644 index 0000000000..843ac90a95 --- /dev/null +++ b/vault/__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, wizards diff --git a/vault/__manifest__.py b/vault/__manifest__.py new file mode 100644 index 0000000000..9154b86381 --- /dev/null +++ b/vault/__manifest__.py @@ -0,0 +1,34 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Vault", + "summary": "Password vault integration in Odoo", + "license": "AGPL-3", + "version": "14.0.1.5.0", + "website": "https://github.com/OCA/server-auth", + "application": True, + "author": "initOS GmbH, Odoo Community Association (OCA)", + "category": "Vault", + "depends": ["web"], + "data": [ + "security/ir.model.access.csv", + "security/ir_rule.xml", + "views/assets.xml", + "views/res_users_views.xml", + "views/vault_entry_views.xml", + "views/vault_field_views.xml", + "views/vault_file_views.xml", + "views/vault_log_views.xml", + "views/vault_inbox_views.xml", + "views/vault_right_views.xml", + "views/vault_views.xml", + "views/menuitems.xml", + "views/templates.xml", + "wizards/vault_export_wizard.xml", + "wizards/vault_import_wizard.xml", + "wizards/vault_send_wizard.xml", + "wizards/vault_store_wizard.xml", + ], + "qweb": ["static/src/xml/templates.xml"], +} diff --git a/vault/controllers/__init__.py b/vault/controllers/__init__.py new file mode 100644 index 0000000000..aabfa83edd --- /dev/null +++ b/vault/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/controllers/main.py b/vault/controllers/main.py new file mode 100644 index 0000000000..3338ad5426 --- /dev/null +++ b/vault/controllers/main.py @@ -0,0 +1,104 @@ +# © 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/inbox/", type="http", auth="public") + def vault_inbox(self, token): + ctx = {"disable_footer": True, "token": token} + # Find the right token + inbox = request.env["vault.inbox"].sudo().find_inbox(token) + user = request.env["res.users"].sudo().find_user_of_inbox(token) + _logger.info("%s: %s", inbox, user) + if len(inbox) == 1 and inbox.accesses > 0: + ctx.update({"name": inbox.name, "public": inbox.user_id.active_key.public}) + elif len(inbox) == 0 and len(user) == 1: + ctx["public"] = user.active_key.public + + # A valid token would mean we found a public key + if not ctx.get("public"): + ctx["error"] = _("Invalid token") + return request.render("vault.inbox", ctx) + + # Just render if GET method + if request.httprequest.method != "POST": + return request.render("vault.inbox", ctx) + + # Check the param + name = request.params.get("name") + secret = request.params.get("encrypted") + secret_file = request.params.get("encrypted_file") + filename = request.params.get("filename") + iv = request.params.get("iv") + key = request.params.get("key") + if not name: + ctx["error"] = _("Please specify a name") + return request.render("vault.inbox", ctx) + + if not secret and not secret_file: + ctx["error"] = _("No secret found") + return request.render("vault.inbox", ctx) + + if secret_file and not filename: + ctx["error"] = _("Missing filename") + return request.render("vault.inbox", ctx) + + if not iv or not key: + ctx["error"] = _("Something went wrong with the encryption") + return request.render("vault.inbox", ctx) + + try: + inbox.store_in_inbox(name, secret, secret_file, iv, key, user, filename) + except Exception as e: + _logger.exception(e) + ctx["error"] = _( + "An error occured. Please contact the user or administrator" + ) + return request.render("vault.inbox", ctx) + + ctx["message"] = _("Successfully stored") + return request.render("vault.inbox", ctx) + + @http.route("/vault/public", type="json") + def vault_public(self, user_id): + """ Get the public key of a specific user """ + user = request.env["res.users"].sudo().browse(user_id).exists() + if not user or not user.keys: + return {} + + return {"public_key": user.active_keys.public} + + @http.route("/vault/keys/store", auth="user", type="json") + def vault_store_keys(self, **kwargs): + """ Store the key pair for the current user """ + return request.env["res.users.key"].store(**kwargs) + + @http.route("/vault/keys/get", auth="user", type="json") + def vault_get_keys(self): + """ Get the currently active key pair """ + return request.env.user.get_vault_keys() + + @http.route("/vault/rights/get", auth="user", type="json") + def vault_get_right_keys(self): + """ Get the master keys from the vault.right records """ + rights = request.env.user.vault_right_ids + return {right.vault_id.uuid: right.key for right in rights} + + @http.route("/vault/rights/store", auth="user", type="json") + def vault_store_right_keys(self, keys): + """ Store the master keys to the specific vault.right records """ + if not isinstance(keys, dict): + return + + for right in request.env.user.vault_right_ids: + master_key = keys.get(right.vault_id.uuid) + + if isinstance(master_key, str): + right.key = master_key diff --git a/vault/models/__init__.py b/vault/models/__init__.py new file mode 100644 index 0000000000..492cc527a4 --- /dev/null +++ b/vault/models/__init__.py @@ -0,0 +1,17 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ( + abstract_vault, + abstract_vault_field, + res_users, + res_users_key, + vault, + vault_entry, + vault_field, + vault_file, + vault_inbox, + vault_log, + vault_right, + vault_tag, +) diff --git a/vault/models/abstract_vault.py b/vault/models/abstract_vault.py new file mode 100644 index 0000000000..4e63e10b93 --- /dev/null +++ b/vault/models/abstract_vault.py @@ -0,0 +1,67 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, models +from odoo.exceptions import AccessError + +_logger = logging.getLogger(__name__) + + +class AbstractVault(models.AbstractModel): + """Models must have the following fields: + `perm_user`: The permissions are computed for this user + `allowed_write`: The current user can read from the vault + `allowed_write`: The current user has write access to the vault + `allowed_share`: The current user can share the vault with other users + `allowed_delete`: The current user can delete the vault or entries of it + """ + + _name = "vault.abstract" + _description = _("Abstract model to implement general access rights") + + @api.model + def raise_access_error(self): + raise AccessError( + _( + "The requested operation can not be completed due to security " + "restrictions." + ) + ) + + def check_access_rule(self, operation): + super().check_access_rule(operation) + + if self.env.su: + return + + # We have to recompute if the user of the environment changed + if self.env.user != self.mapped("perm_user"): + vault = self if self._name == "vault" else self.mapped("vault_id") + vault._compute_access() + + # Check the operation and matching permissions + if operation == "read" and not self.filtered("allowed_read"): + self.raise_access_error() + + if operation == "write" and not self.filtered("allowed_write"): + self.raise_access_error() + + if operation == "unlink" and not self.filtered("allowed_delete"): + self.raise_access_error() + + def _log_entry(self, msg, state): + raise NotImplementedError() + + def log_entry(self, msg): + return self._log_entry(msg, None) + + def log_info(self, msg): + return self._log_entry(msg, "info") + + def log_warn(self, msg): + return self._log_entry(msg, "warn") + + def log_error(self, msg): + return self._log_entry(msg, "error") diff --git a/vault/models/abstract_vault_field.py b/vault/models/abstract_vault_field.py new file mode 100644 index 0000000000..bb062c90a7 --- /dev/null +++ b/vault/models/abstract_vault_field.py @@ -0,0 +1,56 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class AbstractVaultField(models.AbstractModel): + _name = "vault.abstract.field" + _description = _("Abstract model to implement basic fields for encryption") + + entry_id = fields.Many2one("vault.entry", ondelete="cascade", required=True) + vault_id = fields.Many2one(related="entry_id.vault_id") + master_key = fields.Char(compute="_compute_master_key", store=False) + + perm_user = fields.Many2one(related="vault_id.perm_user", store=False) + allowed_read = fields.Boolean(related="vault_id.allowed_read", store=False) + allowed_write = fields.Boolean(related="vault_id.allowed_write", store=False) + allowed_share = fields.Boolean(related="vault_id.allowed_share", store=False) + allowed_delete = fields.Boolean(related="vault_id.allowed_delete", store=False) + + name = fields.Char(required=True) + iv = fields.Char() + + @api.depends("entry_id.vault_id.master_key") + def _compute_master_key(self): + for rec in self: + rec.master_key = rec.vault_id.master_key + + def log_change(self, action): + self.ensure_one() + self.entry_id.log_info( + f"{action} value {self.name} of {self.entry_id.complete_name} " + f"by {self.env.user.display_name}" + ) + + @api.model_create_single + def create(self, values): + res = super().create(values) + res.log_change("Created") + return res + + def unlink(self): + for rec in self: + rec.log_change("Deleted") + + return super().unlink() + + def write(self, values): + for rec in self: + rec.log_change("Changed") + + return super().write(values) diff --git a/vault/models/res_users.py b/vault/models/res_users.py new file mode 100644 index 0000000000..1c836c1914 --- /dev/null +++ b/vault/models/res_users.py @@ -0,0 +1,64 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from uuid import uuid4 + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class ResUsers(models.Model): + _inherit = "res.users" + + active_key = fields.Many2one( + "res.users.key", + compute="_compute_active_key", + store=False, + ) + keys = fields.One2many("res.users.key", "user_id", readonly=True) + vault_right_ids = fields.One2many("vault.right", "user_id", readonly=True) + inbox_enabled = fields.Boolean(default=True) + inbox_link = fields.Char(compute="_compute_inbox_link", readonly=True, store=False) + inbox_token = fields.Char(default=lambda self: uuid4(), readonly=True) + + @api.depends("keys", "keys.current") + def _compute_active_key(self): + for rec in self: + keys = rec.keys.filtered("current") + rec.active_key = keys[0] if keys else None + + @api.depends("inbox_token") + def _compute_inbox_link(self): + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + for rec in self: + rec.inbox_link = f"{base_url}/vault/inbox/{rec.inbox_token}" + + @api.model + def action_get_vault(self): + return self.sudo().env.ref("vault.action_res_users_keys").read()[0] + + def action_new_share_token(self): + self.ensure_one() + self.inbox_token = uuid4() + return {"type": "ir.actions.act_window_close"} + + @api.model + def find_user_of_inbox(self, token): + return self.search([("inbox_token", "=", token), ("inbox_enabled", "=", True)]) + + def get_vault_keys(self): + self.ensure_one() + + if not self.active_key: + return {} + + return { + "iterations": self.active_key.iterations, + "iv": self.active_key.iv, + "private": self.active_key.private, + "public": self.active_key.public, + "salt": self.active_key.salt, + "uuid": self.active_key.uuid, + } diff --git a/vault/models/res_users_key.py b/vault/models/res_users_key.py new file mode 100644 index 0000000000..4fdff0f795 --- /dev/null +++ b/vault/models/res_users_key.py @@ -0,0 +1,75 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import re +from hashlib import sha256 +from uuid import uuid4 + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class ResUsersKey(models.Model): + _name = "res.users.key" + _description = _("User data of a vault") + _rec_name = "fingerprint" + _order = "create_date DESC" + + user_id = fields.Many2one("res.users", required=True) + uuid = fields.Char(default=lambda self: uuid4(), required=True, readonly=True) + current = fields.Boolean(default=True, readonly=True) + fingerprint = fields.Char(compute="_compute_fingerprint", store=True) + public = fields.Char(required=True, readonly=True) + salt = fields.Char(required=True, readonly=True) + iv = fields.Char(required=True, readonly=True) + iterations = fields.Integer(required=True, readonly=True) + # Encrypted with master password of user + private = fields.Char(required=True, readonly=True) + + @api.depends("public") + def _compute_fingerprint(self): + for rec in self: + if rec.public: + hashed = sha256(rec.public.encode()).hexdigest() + rec.fingerprint = ":".join(re.findall(r".{2}", hashed)) + else: + rec.fingerprint = False + + def _prepare_values(self, iterations, iv, private, public, salt): + return { + "iterations": iterations, + "iv": iv, + "private": private, + "public": public, + "salt": salt, + "user_id": self.env.uid, + "current": True, + } + + def store(self, iterations, iv, private, public, salt): + if not all(isinstance(x, str) and x for x in [public, private, iv, salt]): + raise ValidationError(_("Invalid parameter")) + + if not isinstance(iterations, int) or iterations < 4000: + raise ValidationError(_("Invalid parameter")) + + domain = [ + ("user_id", "=", self.env.uid), + ("private", "=", private), + ] + if not self.search(domain): + # Disable all current keys + self.env.user.keys.write({"current": False}) + + rec = self.create( + self._prepare_values(iterations, iv, private, public, salt) + ) + return rec.uuid + return False + + def extract_public_key(self, user): + user = self.sudo().search([("user_id", "=", user), ("current", "=", True)]) + return user.public or None diff --git a/vault/models/vault.py b/vault/models/vault.py new file mode 100644 index 0000000000..873d10fe42 --- /dev/null +++ b/vault/models/vault.py @@ -0,0 +1,158 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from uuid import uuid4 + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class Vault(models.Model): + _name = "vault" + _description = _("Vault") + _inherit = ["vault.abstract"] + _order = "name" + + user_id = fields.Many2one( + "res.users", + "Owner", + readonly=True, + default=lambda self: self.env.user, + required=True, + ) + right_ids = fields.One2many( + "vault.right", + "vault_id", + "Rights", + default=lambda self: self._get_default_rights(), + ) + entry_ids = fields.One2many("vault.entry", "vault_id", "Entries") + field_ids = fields.One2many("vault.field", "vault_id", "Fields") + file_ids = fields.One2many("vault.file", "vault_id", "Files") + log_ids = fields.One2many("vault.log", "vault_id", "Log", readonly=True) + + # Access control + perm_user = fields.Many2one("res.users", compute="_compute_access", store=False) + allowed_read = fields.Boolean(compute="_compute_access", store=False) + allowed_share = fields.Boolean(compute="_compute_access", store=False) + allowed_write = fields.Boolean(compute="_compute_access", store=False) + allowed_delete = fields.Boolean(compute="_compute_access", store=False) + + master_key = fields.Char( + compute="_compute_master_key", + inverse="_inverse_master_key", + store=False, + ) + + uuid = fields.Char(default=lambda self: uuid4(), required=True, readonly=True) + name = fields.Char(required=True) + note = fields.Text() + + _sql_constraints = [ + ("uuid_uniq", "UNIQUE(uuid)", _("The UUID must be unique.")), + ] + + @api.depends("right_ids.user_id") + def _compute_access(self): + user = self.env.user + for rec in self.sudo(): + rec.perm_user = user.id + + if user == rec.user_id: + rec.write( + { + "allowed_share": True, + "allowed_write": True, + "allowed_delete": True, + "allowed_read": True, + } + ) + continue + + rights = rec.right_ids + rec.allowed_read = user in rights.mapped("user_id") + rec.allowed_share = user in rights.filtered("perm_share").mapped("user_id") + rec.allowed_write = user in rights.filtered("perm_write").mapped("user_id") + rec.allowed_delete = user in rights.filtered("perm_delete").mapped( + "user_id" + ) + + @api.depends("right_ids.key") + def _compute_master_key(self): + domain = [("user_id", "=", self.env.uid)] + for rec in self: + rights = rec.right_ids.filtered_domain(domain) + rec.master_key = rights[0].key if rights else False + + def _inverse_master_key(self): + domain = [("user_id", "=", self.env.uid)] + for rec in self: + rights = rec.right_ids.filtered_domain(domain) + if rights and not rights.key: + rights.key = rec.master_key + + def _get_default_rights(self): + return [ + ( + 0, + 0, + { + "user_id": self.env.uid, + "perm_write": True, + "perm_delete": True, + "perm_share": True, + }, + ) + ] + + def _log_entry(self, msg, state): + self.ensure_one() + return ( + self.env["vault.log"] + .sudo() + .create( + { + "vault_id": self.id, + "user_id": self.env.uid, + "message": msg, + "state": state, + } + ) + ) + + def share_public_keys(self): + self.ensure_one() + result = [] + for right in self.right_ids: + result.append({"user": right.user_id.id, "public": right.public_key}) + return result + + def action_open_import_wizard(self): + self.ensure_one() + wizard = self.env.ref("vault.view_vault_import_wizard") + return { + "name": _("Import from file"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "vault.import.wizard", + "views": [(wizard.id, "form")], + "view_id": wizard.id, + "target": "new", + "context": {"default_vault_id": self.id}, + } + + def action_open_export_wizard(self): + self.ensure_one() + wizard = self.env.ref("vault.view_vault_export_wizard") + return { + "name": _("Export to file"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "vault.export.wizard", + "views": [(wizard.id, "form")], + "view_id": wizard.id, + "target": "new", + "context": {"default_vault_id": self.id}, + } diff --git a/vault/models/vault_entry.py b/vault/models/vault_entry.py new file mode 100644 index 0000000000..1e829fe6e4 --- /dev/null +++ b/vault/models/vault_entry.py @@ -0,0 +1,132 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import datetime +from uuid import uuid4 + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class VaultEntry(models.Model): + _name = "vault.entry" + _description = _("Entry inside a vault") + _inherit = ["vault.abstract"] + _order = "complete_name" + _rec_name = "complete_name" + + parent_id = fields.Many2one("vault.entry", "Parent", ondelete="cascade") + child_ids = fields.One2many("vault.entry", "parent_id", "Child") + + vault_id = fields.Many2one("vault", "Vault", ondelete="cascade", required=True) + user_id = fields.Many2one(related="vault_id.user_id") + field_ids = fields.One2many("vault.field", "entry_id", "Fields") + file_ids = fields.One2many("vault.file", "entry_id", "Files") + log_ids = fields.One2many("vault.log", "entry_id", "Log", readonly=True) + + perm_user = fields.Many2one(related="vault_id.perm_user", store=False) + allowed_read = fields.Boolean(related="vault_id.allowed_read", store=False) + allowed_share = fields.Boolean(related="vault_id.allowed_share", store=False) + allowed_write = fields.Boolean(related="vault_id.allowed_write", store=False) + allowed_delete = fields.Boolean(related="vault_id.allowed_delete", store=False) + + complete_name = fields.Char( + compute="_compute_complete_name", + store=True, + readonly=True, + ) + uuid = fields.Char(default=lambda self: uuid4(), required=True) + name = fields.Char(required=True) + url = fields.Char() + note = fields.Text() + tags = fields.Many2many("vault.tag") + expire_date = fields.Datetime("Expires on", default=False) + expired = fields.Boolean(compute="_compute_expired", store=False) + + _sql_constraints = [ + ("vault_uuid_uniq", "UNIQUE(vault_id, uuid)", _("The UUID must be unique.")), + ] + + @api.depends("name", "parent_id.complete_name") + def _compute_complete_name(self): + for rec in self: + if rec.parent_id: + rec.complete_name = f"{rec.parent_id.complete_name} / {rec.name}" + else: + rec.complete_name = rec.name + + @api.depends("expire_date") + def _compute_expired(self): + now = datetime.now() + for rec in self: + rec.expired = rec.expire_date and now > rec.expire_date + + def log_change(self, action): + self.ensure_one() + self.log_info( + f"{action} entry {self.complete_name} by {self.env.user.display_name}" + ) + + @api.model_create_single + def create(self, values): + res = super().create(values) + res.log_change("Created") + return res + + def unlink(self): + for rec in self: + rec.log_change("Deleted") + + return super().unlink() + + def _log_entry(self, msg, state): + self.ensure_one() + return ( + self.env["vault.log"] + .sudo() + .create( + { + "vault_id": self.vault_id.id, + "entry_id": self.id, + "user_id": self.env.uid, + "message": msg, + "state": state, + } + ) + ) + + def action_open_import_wizard(self): + self.ensure_one() + wizard = self.env.ref("vault.view_vault_import_wizard") + return { + "name": _("Import from file"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "vault.import.wizard", + "views": [(wizard.id, "form")], + "view_id": wizard.id, + "target": "new", + "context": { + "default_vault_id": self.vault_id.id, + "default_parent_id": self.id, + }, + } + + def action_open_export_wizard(self): + self.ensure_one() + wizard = self.env.ref("vault.view_vault_export_wizard") + return { + "name": _("Export to file"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "vault.export.wizard", + "views": [(wizard.id, "form")], + "view_id": wizard.id, + "target": "new", + "context": { + "default_vault_id": self.vault_id.id, + "default_entry_id": self.id, + }, + } diff --git a/vault/models/vault_field.py b/vault/models/vault_field.py new file mode 100644 index 0000000000..26490b26e0 --- /dev/null +++ b/vault/models/vault_field.py @@ -0,0 +1,17 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, fields, models + +_logger = logging.getLogger(__name__) + + +class VaultField(models.Model): + _name = "vault.field" + _description = _("Field of a vault") + _order = "name" + _inherit = ["vault.abstract.field", "vault.abstract"] + + value = fields.Char(required=True) diff --git a/vault/models/vault_file.py b/vault/models/vault_file.py new file mode 100644 index 0000000000..6c07e9deef --- /dev/null +++ b/vault/models/vault_file.py @@ -0,0 +1,17 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, fields, models + +_logger = logging.getLogger(__name__) + + +class VaultFile(models.Model): + _name = "vault.file" + _description = _("File of a vault") + _order = "name" + _inherit = ["vault.abstract.field", "vault.abstract"] + + value = fields.Binary(attachment=False) diff --git a/vault/models/vault_inbox.py b/vault/models/vault_inbox.py new file mode 100644 index 0000000000..c2471bfeb4 --- /dev/null +++ b/vault/models/vault_inbox.py @@ -0,0 +1,92 @@ +# © 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 VaultInbox(models.Model): + _name = "vault.inbox" + _description = _("Vault share incoming secrets") + + token = fields.Char(default=lambda self: uuid4(), readonly=True) + inbox_link = fields.Char( + compute="_compute_inbox_link", + readonly=True, + help="Using this link you can write to the current inbox. If you want people " + "to create new inboxes you should give them your inbox link from your key " + "management.", + ) + user_id = fields.Many2one("res.users", "Vault", required=True) + name = fields.Char(required=True) + secret = fields.Char(readonly=True) + filename = fields.Char() + secret_file = fields.Binary(attachment=False, readonly=True) + key = fields.Char(required=True) + iv = fields.Char(required=True) + accesses = fields.Integer( + "Access counter", + default=1, + help="If this is 0 the inbox will be read-only for the owner.", + ) + expiration = fields.Datetime( + default=lambda self: datetime.now() + timedelta(days=7), + help="If expired the inbox will be read-only for the owner.", + ) + + _sql_constraints = [ + ( + "value_check", + "CHECK(secret IS NOT NULL OR secret_file IS NOT NULL)", + _("No value found"), + ), + ] + + @api.depends("token") + def _compute_inbox_link(self): + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + for rec in self: + rec.inbox_link = f"{base_url}/vault/inbox/{rec.token}" + + def read(self, *args, **kwargs): + # Always load the binary instead of the size + return super(VaultInbox, self.with_context(bin_size=False)).read( + *args, **kwargs + ) + + @api.model + def find_inbox(self, token): + return self.search([("token", "=", token)]) + + def store_in_inbox(self, name, secret, secret_file, iv, key, user, filename): + if len(self) == 0: + return self.create( + { + "name": name, + "accesses": 0, + "iv": iv, + "key": key, + "secret": secret or None, + "secret_file": secret_file or None, + "filename": filename, + "user_id": user.id, + } + ) + + if self.accesses > 0 and datetime.now() < self.expiration: + self.write( + { + "accesses": self.accesses - 1, + "iv": iv, + "key": key, + "secret": secret or None, + "secret_file": secret_file or None, + "filename": filename, + } + ) + return self diff --git a/vault/models/vault_log.py b/vault/models/vault_log.py new file mode 100644 index 0000000000..9189dd122d --- /dev/null +++ b/vault/models/vault_log.py @@ -0,0 +1,46 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class VaultLog(models.Model): + _name = "vault.log" + _description = _("Log entry of a vault") + _order = "create_date DESC" + _rec_name = "message" + + vault_id = fields.Many2one( + "vault", + "Vault", + ondelete="cascade", + required=True, + readonly=True, + ) + entry_id = fields.Many2one( + "vault.entry", + "Entry", + ondelete="cascade", + readonly=True, + ) + user_id = fields.Many2one("res.users", "User", required=True, readonly=True) + state = fields.Selection(lambda self: self._get_log_state(), readonly=True) + message = fields.Char(readonly=True, required=True) + + def _get_log_state(self): + return [ + ("info", _("Information")), + ("warn", _("Warning")), + ("error", _("Error")), + ] + + @api.model + def create(self, values): + res = super().create(values) + if not self.env.context.get("skip_log", False): + _logger.info("Vault log: %s", res.message) + return res diff --git a/vault/models/vault_right.py b/vault/models/vault_right.py new file mode 100644 index 0000000000..a4aa6caca2 --- /dev/null +++ b/vault/models/vault_right.py @@ -0,0 +1,100 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class VaultRight(models.Model): + _name = "vault.right" + _description = _("Vault rights") + _inherit = ["vault.abstract"] + _order = "user_id" + + vault_id = fields.Many2one( + "vault", + "Vault", + readonly=True, + required=True, + ondelete="cascade", + ) + master_key = fields.Char(related="vault_id.master_key", readonly=True, store=False) + user_id = fields.Many2one( + "res.users", "User", domain=[("keys", "!=", False)], required=True + ) + public_key = fields.Char(compute="_compute_public_key", readonly=True, store=False) + perm_write = fields.Boolean( + "Write", + default=lambda self: self._get_is_owner(), + help="Allow to write to the vault", + ) + perm_share = fields.Boolean( + "Share", + default=lambda self: self._get_is_owner(), + help="Allow to share a vault with new users", + ) + perm_delete = fields.Boolean( + "Delete", + default=lambda self: self._get_is_owner(), + help="Allow to delete a vault", + ) + + perm_user = fields.Many2one(related="vault_id.perm_user", store=False) + allowed_read = fields.Boolean(related="vault_id.allowed_read", store=False) + allowed_write = fields.Boolean(related="vault_id.allowed_write", store=False) + allowed_share = fields.Boolean(related="vault_id.allowed_share", store=False) + allowed_delete = fields.Boolean(related="vault_id.allowed_delete", store=False) + + # Encrypted with the public key of the user + key = fields.Char() + + _sql_constraints = ( + ("user_uniq", "UNIQUE(user_id, vault_id)", "The user must be unique"), + ) + + def _get_is_owner(self): + return self.env.user == self.vault_id.user_id + + @api.depends("user_id") + def _compute_public_key(self): + for rec in self: + rec.public_key = rec.user_id.active_key.public + + def log_access(self): + self.ensure_one() + rights = ", ".join( + sorted( + ["read"] + + [ + right + for right in ["write", "share", "delete"] + if getattr(self, f"perm_{right}", False) + ] + ) + ) + + self.vault_id.log_info( + f"Grant access to user {self.user_id.display_name}: {rights}" + ) + + @api.model + def create(self, values): + res = super().create(values) + if not res.allowed_share and not res.env.su: + self.raise_access_error() + + res.log_access() + return res + + def write(self, values): + res = super().write(values) + if any(x in values for x in ["perm_write", "perm_delete", "perm_share"]): + for rec in self: + rec.log_access() + + return res + + def unlink(self): + for rec in self: + rec.vault_id.log_info(f"Removed user {self.user_id.display_name}") + + return super().unlink() diff --git a/vault/models/vault_tag.py b/vault/models/vault_tag.py new file mode 100644 index 0000000000..12aa3e6cb2 --- /dev/null +++ b/vault/models/vault_tag.py @@ -0,0 +1,16 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models + + +class VaultTag(models.Model): + _name = "vault.tag" + _description = _("Vault tag") + _order = "name" + + name = fields.Char(required=True) + + _sql_constraints = [ + ("name_uniq", "unique(name)", "The tag must be unique!"), + ] diff --git a/vault/readme/CONTRIBUTORS.rst b/vault/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..e202826121 --- /dev/null +++ b/vault/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Florian Kantelberg diff --git a/vault/readme/DESCRIPTION.rst b/vault/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..ffccef96fe --- /dev/null +++ b/vault/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module implements a vault for secrets and files using end-to-end-encryption. The encryption and decryption happens in the browser using a vault specific shared master key. The master keys are encrypted using asymmetrically. For this the user has to enter a second password on the first login or if he needs to access data in a vault. The asymmetric keys are stored for a certain time in the browser storage. + +The server can never access the secrets with the information available. Only people registered in the vault can decrypt or encrypt values in a vault. The meta data isn't encrypted to be able to search/filter for entries more easily. diff --git a/vault/readme/ROADMAP.rst b/vault/readme/ROADMAP.rst new file mode 100644 index 0000000000..bf394cbda4 --- /dev/null +++ b/vault/readme/ROADMAP.rst @@ -0,0 +1,8 @@ +* Field and file history for restoration + +* Import improvement + + * Support challenge-response/FIDO2 + * Support for argon2 and kdbx v4 + +* Properly handle missing Crypto API because no https diff --git a/vault/security/ir.model.access.csv b/vault/security/ir.model.access.csv new file mode 100644 index 0000000000..3d7db5d0b2 --- /dev/null +++ b/vault/security/ir.model.access.csv @@ -0,0 +1,15 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_vault,access_vault,model_vault,base.group_user,1,1,1,1 +access_vault_entry,access_vault_entry,model_vault_entry,base.group_user,1,1,1,1 +access_vault_export_wizard,access_vault_export_wizard,model_vault_export_wizard,base.group_user,1,1,1,1 +access_vault_field,access_vault_field,model_vault_field,base.group_user,1,1,1,1 +access_vault_file,access_vault_file,model_vault_file,base.group_user,1,1,1,1 +access_vault_import_wizard,access_vault_import_wizard,model_vault_import_wizard,base.group_user,1,1,1,1 +access_vault_import_wizard_path,access_vault_import_wizard_path,model_vault_import_wizard_path,base.group_user,1,1,1,1 +access_vault_inbox,access_vault_inbox,model_vault_inbox,base.group_user,1,1,1,1 +access_vault_log,access_vault_log,model_vault_log,base.group_user,1,0,0,0 +access_vault_right,access_vault_right,model_vault_right,base.group_user,1,1,1,1 +access_vault_send_wizard,access_vault_send_wizard,model_vault_send_wizard,base.group_user,1,1,1,1 +access_vault_store_wizard,access_vault_store_wizard,model_vault_store_wizard,base.group_user,1,1,1,1 +access_vault_tag,access_vault_tag,model_vault_tag,base.group_user,1,1,1,1 +access_vault_users_key,access_res_users_key,model_res_users_key,base.group_user,1,1,1,1 diff --git a/vault/security/ir_rule.xml b/vault/security/ir_rule.xml new file mode 100644 index 0000000000..25707ba7b9 --- /dev/null +++ b/vault/security/ir_rule.xml @@ -0,0 +1,88 @@ + + + + vault.access.default + + ['|', ('user_id', '=', user.id), ('right_ids.user_id', '=', user.id)] + + + + + + + + + vault.log.access.read + + ['|', ('vault_id.user_id', '=', user.id), ('vault_id.right_ids.user_id', '=', user.id)] + + + + + + + + + vault.entry.access.default + + ['|', ('vault_id.user_id', '=', user.id), ('vault_id.right_ids.user_id', '=', user.id)] + + + + + + + + + vault.field.access.default + + ['|', ('vault_id.user_id', '=', user.id), ('vault_id.right_ids.user_id', '=', user.id)] + + + + + + + + + vault.file.access.default + + ['|', ('vault_id.user_id', '=', user.id), ('vault_id.right_ids.user_id', '=', user.id)] + + + + + + + + + res.users.key.access.default + + [('user_id', '=', user.id)] + + + + + + + + + vault.inbox.access.owner + + [('user_id', '=', user.id)] + + + + + + diff --git a/vault/static/description/icon.png b/vault/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|z80;){var r=e%65536;r=r>0?r:65536;var i=new Uint8Array(r);s.getRandomValues(i),e-=r,t.set(i,e)}return t}(e);if(h)return new Uint8Array(h.randomBytes(e));throw new n(o.ErrorCodes.NotImplemented,"Random not implemented")},e.exports.createAesCbc=function(){if(d)return new u;if(h)return new l;throw new n(o.ErrorCodes.NotImplemented,"AES-CBC not implemented")},e.exports.chacha20=function(e,t,r){return Promise.resolve().then((function(){var n=new a(new Uint8Array(t),new Uint8Array(r));return i.arrayToBuffer(n.encrypt(new Uint8Array(e)))}))},e.exports.argon2=function(e,t,r,i,a,s,d,h){return Promise.reject(new n(o.ErrorCodes.NotImplemented,"Argon2 not implemented"))},e.exports.configure=function(e,t,r){d=e,s=t,h=r}}).call(this,r(13))},function(e,t,r){"use strict";(function(t){var i=r(2),n=r(1),o=r(6),a=r(7),s=r(9),d=r(0),h=r(8),u=r(16),l=/\.\d\d\d/,c=t.DOMParser?t:r(44),f=t.DOMParser?void 0:{errorHandler:{error:function(e){throw e},fatalError:function(e){throw e}}},p=62135596800;function m(e){var t,r=f?new c.DOMParser(f):new c.DOMParser;try{t=r.parseFromString(e,"application/xml")}catch(e){throw new i(n.ErrorCodes.FileCorrupt,"bad xml: "+e.message)}if(!t.documentElement)throw new i(n.ErrorCodes.FileCorrupt,"bad xml");var o=t.getElementsByTagName("parsererror")[0];if(o)throw new i(n.ErrorCodes.FileCorrupt,"bad xml: "+o.textContent);return t}function y(e,t){var r=e.childNodes.length;if(0!==r){for(var i,n="\n"+" ".repeat(t),o=t>0?"\n"+" ".repeat(t-1):"",a=e.ownerDocument||e,s=[],d=0;d0){var l=a.createTextNode(o);e.appendChild(l)}y(i,t+1)}}}function g(e){if(e&&e.childNodes)return e.protectedValue?e.protectedValue.text:e.textContent}function _(e,t){e.textContent=t||""}function b(e){var t=g(e);return t?d.arrayToBuffer(d.base64ToBytes(t)):void 0}function v(e,t){"string"==typeof t&&(t=d.base64ToBytes(t)),_(e,t?d.bytesToBase64(d.arrayToBuffer(t)):void 0)}function w(e){switch(e&&e.toLowerCase&&e.toLowerCase()){case"true":return!0;case"false":return!1;case"null":return null}}function k(e,t){t(e);for(var r=0,i=e.childNodes,n=i.length;r)<'+e+"/>")},e.exports.getChildNode=function(e,t,r){if(e&&e.childNodes)for(var o=0,a=e.childNodes,s=a.length;o0)return new Date(t);var r=new DataView(d.arrayToBuffer(d.base64ToBytes(t))),i=new h(r.getUint32(0,!0),r.getUint32(4,!0)).value;return new Date(1e3*(i-p))}},e.exports.setDate=function(e,t,r){if(t)if(r){var i=Math.floor(t.getTime()/1e3)+p,n=new DataView(new ArrayBuffer(8)),o=h.from(i);n.setUint32(0,o.lo,!0),n.setUint32(4,o.hi,!0),_(e,d.bytesToBase64(n.buffer))}else _(e,t.toISOString().replace(l,""));else _(e,"")},e.exports.getNumber=function(e){var t=g(e);return t?+t:void 0},e.exports.setNumber=function(e,t){_(e,"number"!=typeof t||isNaN(t)?void 0:t.toString())},e.exports.getBoolean=function(e){var t=g(e);return t?w(t):void 0},e.exports.setBoolean=function(e,t){_(e,void 0===t?"":null===t?"null":t?"True":"False")},e.exports.strToBoolean=w,e.exports.getUuid=function(e){var t=b(e);return t?new a(t):void 0},e.exports.setUuid=function(e,t){v(e,t instanceof a?t.toBytes():t)},e.exports.getProtectedText=function(e){return e.protectedValue||e.textContent},e.exports.setProtectedText=function(e,t){t instanceof s?(e.protectedValue=t,e.setAttribute(o.Attr.Protected,"True")):_(e,t)},e.exports.getProtectedBinary=function(e){if(e.protectedValue)return e.protectedValue;var t=e.textContent,r=e.getAttribute(o.Attr.Ref);if(r)return{ref:r};if(t){var i=w(e.getAttribute(o.Attr.Compressed)),n=d.base64ToBytes(t);return i&&(n=u.ungzip(n)),d.arrayToBuffer(n)}},e.exports.setProtectedBinary=function(e,t){t instanceof s?(e.protectedValue=t,e.setAttribute(o.Attr.Protected,"True")):t&&t.ref?e.setAttribute(o.Attr.Ref,t.ref):v(e,t)},e.exports.setProtectedValues=function(e,t){k(e,(function(e){if(w(e.getAttribute(o.Attr.Protected)))try{var r=d.arrayToBuffer(d.base64ToBytes(e.textContent));if(r.byteLength){var a=t.getSalt(r.byteLength);e.protectedValue=new s(r,a)}}catch(t){throw new i(n.ErrorCodes.FileCorrupt,"bad protected value at line "+e.lineNumber+": "+t)}}))},e.exports.updateProtectedValuesSalt=function(e,t){k(e,(function(e){if(w(e.getAttribute(o.Attr.Protected))&&e.protectedValue){var r=t.getSalt(e.protectedValue.byteLength);e.protectedValue.setSalt(r),e.textContent=e.protectedValue.toString()}}))},e.exports.unprotectValues=function(e){k(e,(function(e){w(e.getAttribute(o.Attr.Protected))&&e.protectedValue&&(e.removeAttribute(o.Attr.Protected),e.setAttribute(o.Attr.ProtectedInMemPlainXml,"True"),e.textContent=e.protectedValue.getText())}))},e.exports.protectUnprotectedValues=function(e){k(e,(function(e){w(e.getAttribute(o.Attr.ProtectedInMemPlainXml))&&e.protectedValue&&(e.removeAttribute(o.Attr.ProtectedInMemPlainXml),e.setAttribute(o.Attr.Protected,"True"),e.textContent=e.protectedValue.toString())}))},e.exports.protectPlainValues=function(e){k(e,(function(e){w(e.getAttribute(o.Attr.ProtectedInMemPlainXml))&&(e.protectedValue=s.fromString(e.textContent),e.textContent=e.protectedValue.toString(),e.removeAttribute(o.Attr.ProtectedInMemPlainXml),e.setAttribute(o.Attr.Protected,"True"))}))}}).call(this,r(13))},function(e,t,r){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;t.assign=function(e){for(var t=Array.prototype.slice.call(arguments,1);t.length;){var r=t.shift();if(r){if("object"!=typeof r)throw new TypeError(r+"must be non-object");for(var i in r)r.hasOwnProperty(i)&&(e[i]=r[i])}}return e},t.shrinkBuf=function(e,t){return e.length===t?e:e.subarray?e.subarray(0,t):(e.length=t,e)};var n={arraySet:function(e,t,r,i,n){if(t.subarray&&e.subarray)e.set(t.subarray(r,r+i),n);else for(var o=0;o=2097152)throw new Error("too large number");return 4294967296*this.hi+this.lo}return this.lo}}),i.prototype.valueOf=function(){return this.value},i.from=function(e){if(e>9007199254740991)throw new Error("too large number");var t=e>>>0;return new i(t,(e-t)/4294967296>>>0)},e.exports=i},function(e,t,r){"use strict";var i=r(0),n=r(3),o=r(10),a=function(e,t){Object.defineProperty(this,"_value",{value:new Uint8Array(e)}),Object.defineProperty(this,"_salt",{value:new Uint8Array(t)})};a.prototype.toString=function(){return i.bytesToBase64(this._value)},a.fromString=function(e){for(var t=i.stringToBytes(e),r=o.getBytes(t.length),n=0,s=t.length;n=0;i--)r[i]=e[i]^t[i];return r},a.prototype.setSalt=function(e){for(var t=new Uint8Array(e),r=this._value,i=this._salt,n=0,o=r.length;n=0;--i)t[i]^=r[i];return t}},function(e,t,r){"use strict";function i(e){this._arrayBuffer=e||new ArrayBuffer(1024),this._dataView=new DataView(this._arrayBuffer),this._pos=0,this._canExpand=!e}["Int","Uint","Float"].forEach((function(e){("Float"===e?[4,8]:[1,2,4]).forEach((function(t){var r="get"+e+8*t;i.prototype[r]=function(e){var i=this._dataView[r].call(this._dataView,this._pos,e);return this._pos+=t,i};var n="set"+e+8*t;i.prototype[n]=function(e,r){this._checkCapacity(t),this._dataView[n].call(this._dataView,this._pos,e,r),this._pos+=t}}))})),i.prototype.getUint64=function(e){var t=this.getUint32(e),r=this.getUint32(e);return e?r*=4294967296:t*=4294967296,t+r},i.prototype.setUint64=function(e,t){t?(this.setUint32(4294967295&e,!0),this.setUint32(Math.floor(e/4294967296),!0)):(this._checkCapacity(8),this.setUint32(Math.floor(e/4294967296),!1),this.setUint32(4294967295&e,!1))},i.prototype.readBytes=function(e){var t=this._arrayBuffer.slice(this._pos,this._pos+e);return this._pos+=e,t},i.prototype.readBytesToEnd=function(){var e=this._arrayBuffer.byteLength-this._pos;return this.readBytes(e)},i.prototype.readBytesNoAdvance=function(e,t){return this._arrayBuffer.slice(e,t)},i.prototype.writeBytes=function(e){e instanceof ArrayBuffer&&(e=new Uint8Array(e)),this._checkCapacity(e.length),new Uint8Array(this._arrayBuffer).set(e,this._pos),this._pos+=e.length},i.prototype.getWrittenBytes=function(){return this._arrayBuffer.slice(0,this._pos)},i.prototype._checkCapacity=function(e){var t=this._arrayBuffer.byteLength-this._pos;if(this._canExpand&&t1)throw new i(n.ErrorCodes.InvalidVersion)},d.prototype._readItem=function(e){var t=e.getUint8();if(!t)return!1;var r=e.getInt32(!0);if(r<=0)throw new i(n.ErrorCodes.FileCorrupt,"bad key length");var d,h=o.bytesToString(e.readBytes(r)),u=e.getInt32(!0);if(u<0)throw new i(n.ErrorCodes.FileCorrupt,"bad value length");switch(t){case s.UInt32:if(4!==u)throw new i(n.ErrorCodes.FileCorrupt,"bad uint32");d=e.getUint32(!0);break;case s.UInt64:if(8!==u)throw new i(n.ErrorCodes.FileCorrupt,"bad uint64");var l=e.getUint32(!0),c=e.getUint32(!0);d=new a(l,c);break;case s.Bool:if(1!==u)throw new i(n.ErrorCodes.FileCorrupt,"bad bool");d=0!==e.getUint8();break;case s.Int32:if(4!==u)throw new i(n.ErrorCodes.FileCorrupt,"bad int32");d=e.getInt32(!0);break;case s.Int64:if(8!==u)throw new i(n.ErrorCodes.FileCorrupt,"bad int64");var f=e.getUint32(!0),p=e.getUint32(!0);d=new a(f,p);break;case s.String:d=o.bytesToString(e.readBytes(u));break;case s.Bytes:d=e.readBytes(u);break;default:throw new i(n.ErrorCodes.FileCorrupt,"bad value type: "+t)}return{key:h,type:t,value:d}},d.prototype.write=function(e){this._writeVersion(e),Object.keys(this._items).forEach((function(t){this._writeItem(e,this._items[t])}),this),e.setUint8(0)},d.prototype._writeVersion=function(e){e.setUint16(256,!0)},d.prototype._writeItem=function(e,t){e.setUint8(t.type);var r=o.stringToBytes(t.key);switch(e.setInt32(r.length,!0),e.writeBytes(r),t.type){case s.UInt32:e.setInt32(4,!0),e.setUint32(t.value,!0);break;case s.UInt64:e.setInt32(8,!0),e.setUint32(t.value.lo,!0),e.setUint32(t.value.hi,!0);break;case s.Bool:e.setInt32(1,!0),e.setUint8(t.value?1:0);break;case s.Int32:e.setInt32(4,!0),e.setInt32(t.value,!0);break;case s.Int64:e.setInt32(8,!0),e.setUint32(t.value.lo,!0),e.setUint32(t.value.hi,!0);break;case s.String:var a=o.stringToBytes(t.value);e.setInt32(a.length,!0),e.writeBytes(a);break;case s.Bytes:var d=o.arrayToBuffer(t.value);e.setInt32(d.byteLength,!0),e.writeBytes(d);break;default:throw new i(n.ErrorCodes.Unsupported)}},e.exports=d},function(e,t,r){"use strict";var i=r(6),n=r(4),o={read:function(e){for(var t={},r=0,n=e.childNodes,a=n.length;r>>16&65535|0,a=0;0!==r;){r-=a=r>2e3?2e3:r;do{o=o+(n=n+t[i++]|0)|0}while(--a);n%=65521,o%=65521}return n|o<<16|0}},function(e,t,r){"use strict";var i=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var i=0;i<8;i++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();e.exports=function(e,t,r,n){var o=i,a=n+r;e^=-1;for(var s=n;s>>8^o[255&(e^t[s])];return-1^e}},function(e,t,r){"use strict";var i=r(5),n=!0,o=!0;try{String.fromCharCode.apply(null,[0])}catch(e){n=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(e){o=!1}for(var a=new i.Buf8(256),s=0;s<256;s++)a[s]=s>=252?6:s>=248?5:s>=240?4:s>=224?3:s>=192?2:1;function d(e,t){if(t<65537&&(e.subarray&&o||!e.subarray&&n))return String.fromCharCode.apply(null,i.shrinkBuf(e,t));for(var r="",a=0;a>>6,t[a++]=128|63&r):r<65536?(t[a++]=224|r>>>12,t[a++]=128|r>>>6&63,t[a++]=128|63&r):(t[a++]=240|r>>>18,t[a++]=128|r>>>12&63,t[a++]=128|r>>>6&63,t[a++]=128|63&r);return t},t.buf2binstring=function(e){return d(e,e.length)},t.binstring2buf=function(e){for(var t=new i.Buf8(e.length),r=0,n=t.length;r4)h[i++]=65533,r+=o-1;else{for(n&=2===o?31:3===o?15:7;o>1&&r1?h[i++]=65533:n<65536?h[i++]=n:(n-=65536,h[i++]=55296|n>>10&1023,h[i++]=56320|1023&n)}return d(h,i)},t.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;r>=0&&128==(192&e[r]);)r--;return r<0||0===r?t:r+a[e[r]]>t?r:t}},function(e,t,r){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},function(e,t,r){"use strict";e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},function(e,t,r){"use strict";var i=r(7),n=r(1),o=r(9),a=r(2),s=r(11),d=r(0),h=r(14),u=r(8),l=r(10),c=[{name:"EndOfHeader"},{name:"Comment"},{name:"CipherID"},{name:"CompressionFlags"},{name:"MasterSeed"},{name:"TransformSeed",ver:[3]},{name:"TransformRounds",ver:[3]},{name:"EncryptionIV"},{name:"ProtectedStreamKey",ver:[3]},{name:"StreamStartBytes",ver:[3]},{name:"InnerRandomStreamID",ver:[3]},{name:"KdfParameters",ver:[4]},{name:"PublicCustomData",ver:[4]}],f=[{name:"EndOfHeader"},{name:"InnerRandomStreamID"},{name:"InnerRandomStreamKey"},{name:"Binary",skipHeader:!0}],p={DefaultFileVersionMajor:4,DefaultFileVersionMinor:0,MaxFileVersionMajor:4,MaxFileVersionMinor:1,MaxSupportedVersion:4,FlagBinaryProtected:1,InnerHeaderBinaryFieldId:3,DefaultKdfAlgo:n.KdfId.Argon2,DefaultKdfSaltLength:32,DefaultKdfParallelism:1,DefaultKdfIterations:2,DefaultKdfMemory:1048576,DefaultKdfVersion:19},m={3:1,4:0},y=function(){this.versionMajor=void 0,this.versionMinor=void 0,this.dataCipherUuid=void 0,this.compression=void 0,this.masterSeed=void 0,this.transformSeed=void 0,this.keyEncryptionRounds=void 0,this.encryptionIV=void 0,this.protectedStreamKey=void 0,this.streamStartBytes=void 0,this.crsAlgorithm=void 0,this.endPos=void 0,this.kdfParameters=void 0,this.publicCustomData=void 0,Object.preventExtensions(this)};y.prototype._readSignature=function(e){if(e.byteLength<8)throw new a(n.ErrorCodes.FileCorrupt,"not enough data");var t=e.getUint32(!0),r=e.getUint32(!0);if(t!==n.Signatures.FileMagic||r!==n.Signatures.Sig2Kdbx)throw new a(n.ErrorCodes.BadSignature)},y.prototype._writeSignature=function(e){e.setUint32(n.Signatures.FileMagic,!0),e.setUint32(n.Signatures.Sig2Kdbx,!0)},y.prototype._readVersion=function(e){var t=e.getUint16(!0),r=e.getUint16(!0);if(r>p.MaxSupportedVersion)throw new a(n.ErrorCodes.InvalidVersion);this.versionMinor=t,this.versionMajor=r},y.prototype._writeVersion=function(e){e.setUint16(this.versionMinor,!0),e.setUint16(this.versionMajor,!0)},y.prototype._readCipherID=function(e){if(16!==e.byteLength)throw new a(n.ErrorCodes.Unsupported,"cipher");this.dataCipherUuid=new i(e)},y.prototype._writeCipherID=function(e){this._writeFieldSize(e,16),e.writeBytes(this.dataCipherUuid.bytes)},y.prototype._readCompressionFlags=function(e){var t=new DataView(e).getUint32(e,!0);if(t<0||t>=Object.keys(n.CompressionAlgorithm).length)throw new a(n.ErrorCodes.Unsupported,"compression");this.compression=t},y.prototype._writeCompressionFlags=function(e){this._writeFieldSize(e,4),e.setUint32(this.compression,!0)},y.prototype._readMasterSeed=function(e){this.masterSeed=e},y.prototype._writeMasterSeed=function(e){this._writeFieldBytes(e,this.masterSeed)},y.prototype._readTransformSeed=function(e){this.transformSeed=e},y.prototype._writeTransformSeed=function(e){this._writeFieldBytes(e,this.transformSeed)},y.prototype._readTransformRounds=function(e){this.keyEncryptionRounds=new s(e).getUint64(!0)},y.prototype._writeTransformRounds=function(e){this._writeFieldSize(e,8),e.setUint64(this.keyEncryptionRounds,!0)},y.prototype._readEncryptionIV=function(e){this.encryptionIV=e},y.prototype._writeEncryptionIV=function(e){this._writeFieldBytes(e,this.encryptionIV)},y.prototype._readProtectedStreamKey=function(e){this.protectedStreamKey=e},y.prototype._writeProtectedStreamKey=function(e){this._writeFieldBytes(e,this.protectedStreamKey)},y.prototype._readStreamStartBytes=function(e){this.streamStartBytes=e},y.prototype._writeStreamStartBytes=function(e){this._writeFieldBytes(e,this.streamStartBytes)},y.prototype._readInnerRandomStreamID=function(e){this.crsAlgorithm=new DataView(e).getUint32(e,!0)},y.prototype._writeInnerRandomStreamID=function(e){this._writeFieldSize(e,4),e.setUint32(this.crsAlgorithm,!0)},y.prototype._readInnerRandomStreamKey=function(e){this.protectedStreamKey=e},y.prototype._writeInnerRandomStreamKey=function(e){this._writeFieldBytes(e,this.protectedStreamKey)},y.prototype._readKdfParameters=function(e){this.kdfParameters=h.read(new s(e))},y.prototype._writeKdfParameters=function(e){var t=new s;this.kdfParameters.write(t),this._writeFieldBytes(e,t.getWrittenBytes())},y.prototype._readPublicCustomData=function(e){this.publicCustomData=h.read(new s(e))},y.prototype._hasPublicCustomData=function(){return this.publicCustomData},y.prototype._writePublicCustomData=function(e){if(this.publicCustomData){var t=new s;this.publicCustomData.write(t),this._writeFieldBytes(e,t.getWrittenBytes())}},y.prototype._readBinary=function(e,t){var r=new DataView(e).getUint8(0)&p.FlagBinaryProtected,i=e.slice(1),n=r?o.fromBinary(i):i,a=Object.keys(t.kdbx.binaries).length;t.kdbx.binaries[a]=n},y.prototype._writeBinary=function(e,t){if(!(this.versionMajor<4))for(var r=t.kdbx.binaries.hashOrder,i=0;i0&&(i=e.readBytes(o));var a=t[n];if(a){var s=this["_read"+a.name];s&&s.call(this,i,r)}return 0!==n},y.prototype._writeField=function(e,t,r,i){var n=r[t];if(n){if(n.ver&&n.ver.indexOf(this.versionMajor)<0)return;var o=this["_write"+n.name];if(o){var a=this["_has"+n.name];if(a&&!a.call(this))return;n.skipHeader||e.setUint8(t),o.call(this,e,i)}}},y.prototype._readFieldSize=function(e){return this.versionMajor>=4?e.getUint32(!0):e.getUint16(!0)},y.prototype._writeFieldSize=function(e,t){this.versionMajor>=4?e.setUint32(t,!0):e.setUint16(t,!0)},y.prototype._writeFieldBytes=function(e,t){this._writeFieldSize(e,t.byteLength),e.writeBytes(t)},y.prototype._validate=function(){if(void 0===this.dataCipherUuid)throw new a(n.ErrorCodes.FileCorrupt,"no cipher in header");if(void 0===this.compression)throw new a(n.ErrorCodes.FileCorrupt,"no compression in header");if(!this.masterSeed)throw new a(n.ErrorCodes.FileCorrupt,"no master seed in header");if(this.versionMajor<4&&!this.transformSeed)throw new a(n.ErrorCodes.FileCorrupt,"no transform seed in header");if(this.versionMajor<4&&!this.keyEncryptionRounds)throw new a(n.ErrorCodes.FileCorrupt,"no key encryption rounds in header");if(!this.encryptionIV)throw new a(n.ErrorCodes.FileCorrupt,"no encryption iv in header");if(this.versionMajor<4&&!this.protectedStreamKey)throw new a(n.ErrorCodes.FileCorrupt,"no protected stream key in header");if(this.versionMajor<4&&!this.streamStartBytes)throw new a(n.ErrorCodes.FileCorrupt,"no stream start bytes in header");if(this.versionMajor<4&&!this.crsAlgorithm)throw new a(n.ErrorCodes.FileCorrupt,"no crs algorithm in header");if(this.versionMajor>=4&&!this.kdfParameters)throw new a(n.ErrorCodes.FileCorrupt,"no kdf parameters in header")},y.prototype._validateInner=function(){if(!this.protectedStreamKey)throw new a(n.ErrorCodes.FileCorrupt,"no protected stream key in header");if(!this.crsAlgorithm)throw new a(n.ErrorCodes.FileCorrupt,"no crs algorithm in header")},y.prototype._createKdfParameters=function(e){switch(e||(e=p.DefaultKdfAlgo),e){case n.KdfId.Argon2:this.kdfParameters=new h,this.kdfParameters.set("$UUID",h.ValueType.Bytes,d.base64ToBytes(n.KdfId.Argon2)),this.kdfParameters.set("S",h.ValueType.Bytes,l.getBytes(p.DefaultKdfSaltLength)),this.kdfParameters.set("P",h.ValueType.UInt32,p.DefaultKdfParallelism),this.kdfParameters.set("I",h.ValueType.UInt64,new u(p.DefaultKdfIterations)),this.kdfParameters.set("M",h.ValueType.UInt64,new u(p.DefaultKdfMemory)),this.kdfParameters.set("V",h.ValueType.UInt32,p.DefaultKdfVersion);break;case n.KdfId.Aes:this.kdfParameters=new h,this.kdfParameters.set("$UUID",h.ValueType.Bytes,d.base64ToBytes(n.KdfId.Aes)),this.kdfParameters.set("S",h.ValueType.Bytes,l.getBytes(p.DefaultKdfSaltLength)),this.kdfParameters.set("R",h.ValueType.UInt64,new u(n.Defaults.KeyEncryptionRounds));break;default:throw new a(n.ErrorCodes.InvalidArg,"bad KDF algo")}},y.prototype.write=function(e){this._validate(),this._writeSignature(e),this._writeVersion(e);for(var t=1;t>4&15]),r.push(t[15&i[n]]);return r.join("")},i.prototype._reset=function(){this.counterWords[0]=0,this.counterWords[1]=0,this.blockUsed=64},i.prototype._incrementCounter=function(){this.counterWords[0]=this.counterWords[0]+1&4294967295,0===this.counterWords[0]&&(this.counterWords[1]=this.counterWords[1]+1&4294967295)},i.prototype._generateBlock=function(){for(var e,t=this.sigmaWords[0],r=this.keyWords[0],i=this.keyWords[1],n=this.keyWords[2],o=this.keyWords[3],a=this.sigmaWords[1],s=this.nonceWords[0],d=this.nonceWords[1],h=this.counterWords[0],u=this.counterWords[1],l=this.sigmaWords[2],c=this.keyWords[4],f=this.keyWords[5],p=this.keyWords[6],m=this.keyWords[7],y=this.sigmaWords[3],g=t,_=r,b=i,v=n,w=o,k=a,C=s,x=d,E=h,B=u,T=l,A=c,S=f,U=p,D=m,I=y,N=0;N>>25)+g)<<9|e>>>23)+w)<<13|e>>>19)+E)<<18|e>>>14,k^=(e=(_^=(e=(U^=(e=(B^=(e=k+_)<<7|e>>>25)+k)<<9|e>>>23)+B)<<13|e>>>19)+U)<<18|e>>>14,T^=(e=(C^=(e=(b^=(e=(D^=(e=T+C)<<7|e>>>25)+T)<<9|e>>>23)+D)<<13|e>>>19)+b)<<18|e>>>14,I^=(e=(A^=(e=(x^=(e=(v^=(e=I+A)<<7|e>>>25)+I)<<9|e>>>23)+v)<<13|e>>>19)+x)<<18|e>>>14,g^=(e=(v^=(e=(b^=(e=(_^=(e=g+v)<<7|e>>>25)+g)<<9|e>>>23)+_)<<13|e>>>19)+b)<<18|e>>>14,k^=(e=(w^=(e=(x^=(e=(C^=(e=k+w)<<7|e>>>25)+k)<<9|e>>>23)+C)<<13|e>>>19)+x)<<18|e>>>14,T^=(e=(B^=(e=(E^=(e=(A^=(e=T+B)<<7|e>>>25)+T)<<9|e>>>23)+A)<<13|e>>>19)+E)<<18|e>>>14,I^=(e=(D^=(e=(U^=(e=(S^=(e=I+D)<<7|e>>>25)+I)<<9|e>>>23)+S)<<13|e>>>19)+U)<<18|e>>>14;g+=t,_+=r,b+=i,v+=n,w+=o,k+=a,C+=s,x+=d,E+=h,B+=u,T+=l,A+=c,S+=f,U+=p,D+=m,I+=y,this.block[0]=g>>>0&255,this.block[1]=g>>>8&255,this.block[2]=g>>>16&255,this.block[3]=g>>>24&255,this.block[4]=_>>>0&255,this.block[5]=_>>>8&255,this.block[6]=_>>>16&255,this.block[7]=_>>>24&255,this.block[8]=b>>>0&255,this.block[9]=b>>>8&255,this.block[10]=b>>>16&255,this.block[11]=b>>>24&255,this.block[12]=v>>>0&255,this.block[13]=v>>>8&255,this.block[14]=v>>>16&255,this.block[15]=v>>>24&255,this.block[16]=w>>>0&255,this.block[17]=w>>>8&255,this.block[18]=w>>>16&255,this.block[19]=w>>>24&255,this.block[20]=k>>>0&255,this.block[21]=k>>>8&255,this.block[22]=k>>>16&255,this.block[23]=k>>>24&255,this.block[24]=C>>>0&255,this.block[25]=C>>>8&255,this.block[26]=C>>>16&255,this.block[27]=C>>>24&255,this.block[28]=x>>>0&255,this.block[29]=x>>>8&255,this.block[30]=x>>>16&255,this.block[31]=x>>>24&255,this.block[32]=E>>>0&255,this.block[33]=E>>>8&255,this.block[34]=E>>>16&255,this.block[35]=E>>>24&255,this.block[36]=B>>>0&255,this.block[37]=B>>>8&255,this.block[38]=B>>>16&255,this.block[39]=B>>>24&255,this.block[40]=T>>>0&255,this.block[41]=T>>>8&255,this.block[42]=T>>>16&255,this.block[43]=T>>>24&255,this.block[44]=A>>>0&255,this.block[45]=A>>>8&255,this.block[46]=A>>>16&255,this.block[47]=A>>>24&255,this.block[48]=S>>>0&255,this.block[49]=S>>>8&255,this.block[50]=S>>>16&255,this.block[51]=S>>>24&255,this.block[52]=U>>>0&255,this.block[53]=U>>>8&255,this.block[54]=U>>>16&255,this.block[55]=U>>>24&255,this.block[56]=D>>>0&255,this.block[57]=D>>>8&255,this.block[58]=D>>>16&255,this.block[59]=D>>>24&255,this.block[60]=I>>>0&255,this.block[61]=I>>>8&255,this.block[62]=I>>>16&255,this.block[63]=I>>>24&255},e.exports=i},function(e,t,r){"use strict";function i(e,t){this.sigmaWords=[1634760805,857760878,2036477234,1797285236],this.block=new Uint8Array(64),this.blockUsed=64,this.x=new Uint32Array(16);var r=new Uint32Array(16);r[0]=this.sigmaWords[0],r[1]=this.sigmaWords[1],r[2]=this.sigmaWords[2],r[3]=this.sigmaWords[3],r[4]=o(e,0),r[5]=o(e,4),r[6]=o(e,8),r[7]=o(e,12),r[8]=o(e,16),r[9]=o(e,20),r[10]=o(e,24),r[11]=o(e,28),r[12]=0,12===t.length?(r[13]=o(t,0),r[14]=o(t,4),r[15]=o(t,8)):(r[13]=0,r[14]=o(t,0),r[15]=o(t,4)),this.input=r}function n(e,t,r,i,n){e[t]+=e[r],e[n]=s(e[n]^e[t],16),e[i]+=e[n],e[r]=s(e[r]^e[i],12),e[t]+=e[r],e[n]=s(e[n]^e[t],8),e[i]+=e[n],e[r]=s(e[r]^e[i],7)}function o(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function a(e,t,r){e[t]=r,r>>>=8,e[t+1]=r,r>>>=8,e[t+2]=r,r>>>=8,e[t+3]=r}function s(e,t){return e<>>32-t}i.prototype.getBytes=function(e){for(var t=new Uint8Array(e),r=0;r0;e-=2)n(r,0,4,8,12),n(r,1,5,9,13),n(r,2,6,10,14),n(r,3,7,11,15),n(r,0,5,10,15),n(r,1,6,11,12),n(r,2,7,8,13),n(r,3,4,9,14);for(e=16;e--;)r[e]+=t[e];for(e=16;e--;)a(i,4*e,r[e]);t[12]+=1,t[12]||(t[13]+=1)},i.prototype.encrypt=function(e){for(var t=e.length,r=new Uint8Array(t),i=0,n=this.block;i0;){var d=Math.min(r,1e4);r-=d;var h=o*d;n=s(e,n,a.length===h?a.buffer:i.arrayToBuffer(a.subarray(0,h)))}return n.then((function(e){return new Uint8Array(e)}))}function s(e,t,r){return t.then((function(t){return e.encrypt(r,t)})).then((function(e){var t=i.arrayToBuffer(new Uint8Array(e).subarray(-32,-16));return i.zeroBuffer(e),t}))}e.exports.encrypt=function(e,t,r){var s=n.createAesCbc();return s.importKey(i.arrayToBuffer(t)).then((function(){for(var t=[],i=0;i<32;i+=o)t.push(a(s,e.subarray(i,i+o),r));return Promise.all(t)})).then((function(e){var t=new Uint8Array(32);return e.forEach((function(e,r){for(var n=r*o,a=0;a\n \n";return a.stringToBytes(t)},e.exports=u},function(e,t,r){"use strict";var i=r(6),n=r(4),o=function(){this.creationTime=void 0,this.lastModTime=void 0,this.lastAccessTime=void 0,this.expiryTime=void 0,this.expires=void 0,this.usageCount=void 0,this.locationChanged=new Date,Object.preventExtensions(this)};o.prototype._readNode=function(e){switch(e.tagName){case i.Elem.CreationTime:this.creationTime=n.getDate(e);break;case i.Elem.LastModTime:this.lastModTime=n.getDate(e);break;case i.Elem.LastAccessTime:this.lastAccessTime=n.getDate(e);break;case i.Elem.ExpiryTime:this.expiryTime=n.getDate(e);break;case i.Elem.Expires:this.expires=n.getBoolean(e);break;case i.Elem.UsageCount:this.usageCount=n.getNumber(e);break;case i.Elem.LocationChanged:this.locationChanged=n.getDate(e)}},o.prototype.clone=function(){var e=new o;return e.creationTime=this.creationTime,e.lastModTime=this.lastModTime,e.lastAccessTime=this.lastAccessTime,e.expiryTime=this.expiryTime,e.expires=this.expires,e.usageCount=this.usageCount,e.locationChanged=this.locationChanged,e},o.prototype.update=function(){var e=new Date;this.lastModTime=e,this.lastAccessTime=e},o.prototype.write=function(e,t){var r=n.addChildNode(e,i.Elem.Times);t.setXmlDate(n.addChildNode(r,i.Elem.CreationTime),this.creationTime),t.setXmlDate(n.addChildNode(r,i.Elem.LastModTime),this.lastModTime),t.setXmlDate(n.addChildNode(r,i.Elem.LastAccessTime),this.lastAccessTime),t.setXmlDate(n.addChildNode(r,i.Elem.ExpiryTime),this.expiryTime),n.setBoolean(n.addChildNode(r,i.Elem.Expires),this.expires),n.setNumber(n.addChildNode(r,i.Elem.UsageCount),this.usageCount),t.setXmlDate(n.addChildNode(r,i.Elem.LocationChanged),this.locationChanged)},o.create=function(){var e=new o,t=new Date;return e.creationTime=t,e.lastModTime=t,e.lastAccessTime=t,e.expiryTime=t,e.expires=!1,e.usageCount=0,e.locationChanged=t,e},o.read=function(e){for(var t=new o,r=0,i=e.childNodes,n=i.length;rt.times.lastModTime){if(!this.history.some((function(e){return+e.times.lastModTime==+t.times.lastModTime}))){var i=new l;i.copyFrom(t),r.push(i)}}this.history=this._mergeHistory(r,t.times.lastModTime)}},l.prototype._mergeHistory=function(e,t){this.history.sort((function(e,t){return e.times.lastModTime-t.times.lastModTime})),e.sort((function(e,t){return e.times.lastModTime-t.times.lastModTime}));var r={},i={};this.history.forEach((function(e){r[e.times.lastModTime.getTime()]=e})),e.forEach((function(e){i[e.times.lastModTime.getTime()]=e}));for(var n=0,o=0,a=[];nu){if(!this._editState||this._editState.deleted.indexOf(u)<0){var c=new l;c.copyFrom(d),a.push(c)}o++}else(this._editState&&this._editState.added.indexOf(h)>=0||h>t)&&a.push(s),n++;else a.push(s),n++,o++}return a},l.create=function(e,t){var r=new l(t);return r.uuid=d.random(),r.icon=a.Icons.Key,r.times=h.create(),r.parentGroup=t,r._setField("Title","",e.memoryProtection.title),r._setField("UserName",e.defaultUser||"",e.memoryProtection.userName),r._setField("Password","",e.memoryProtection.password),r._setField("URL","",e.memoryProtection.url),r._setField("Notes","",e.memoryProtection.notes),r.autoType.enabled="boolean"!=typeof t.enableAutoType||t.enableAutoType,r.autoType.obfuscation=a.AutoTypeObfuscationOptions.None,r},l.read=function(e,t,r){for(var i=new l,n=0,o=e.childNodes,a=o.length;n=0?t[i].splice(r,0,e):t[i].push(e);else{var a=new Date;e instanceof h?e.forEach((function(e,t){this.addDeletedObject((e||t).uuid,a)}),this):this.addDeletedObject(e.uuid,a)}e.parentGroup=t,e.times.locationChanged=new Date}},y.prototype.addDeletedObject=function(e,t){var r=new l;r.uuid=e,r.deletionTime=t,this.deletedObjects.push(r)},y.prototype.remove=function(e){var t=null;this.meta.recycleBinEnabled&&(this.createRecycleBin(),t=this.getGroup(this.meta.recycleBinUuid)),this.move(e,t)},y.prototype.createBinary=function(e){return this.binaries.add(e)},y.prototype.importEntry=function(e,t,r){var i=new u,n=c.random();i.copyFrom(e),i.uuid=n,e.history.forEach((function(e){var t=new u;t.copyFrom(e),t.uuid=n,i.history.push(t)}));var o={},a={};return i.history.concat(i).forEach((function(e){e.customIcon&&(a[e.customIcon]=e.customIcon),Object.values(e.binaries).forEach((function(e){e.ref&&(o[e.ref]=e)}))})),Object.values(o).forEach((function(e){var t=r.binaries[e.ref];t&&!this.binaries[e.ref]&&(this.binaries[e.ref]=t)}),this),Object.values(a).forEach((function(e){var t=r.meta.customIcons[e];t&&(this.meta.customIcons[e]=t)}),this),t.entries.push(i),i.parentGroup=t,i.times.update(),i},y.prototype.cleanup=function(e){var t=new Date,r=e&&e.historyRules&&"number"==typeof this.meta.historyMaxItems&&this.meta.historyMaxItems>=0?this.meta.historyMaxItems:1/0,i={},n={},o=function(e){e&&e.customIcon&&(i[e.customIcon]=!0),e&&e.binaries&&Object.keys(e.binaries).forEach((function(t){e.binaries[t]&&e.binaries[t].ref&&(n[e.binaries[t].ref]=!0)}))};this.getDefaultGroup().forEach((function(e,t){e&&e.history.length>r&&e.removeHistory(0,e.history.length-r),e&&o(e),e&&e.history&&e.history.forEach((function(e){o(e)})),t&&t.customIcon&&(i[t.customIcon]=!0)})),e&&e.customIcons&&Object.keys(this.meta.customIcons).forEach((function(e){if(!i[e]){var r=new c(e);this.addDeletedObject(r,t),delete this.meta.customIcons[e]}}),this),e&&e.binaries&&Object.keys(this.binaries).forEach((function(e){n[e]||delete this.binaries[e]}),this)},y.prototype.merge=function(e){var t=this.getDefaultGroup(),r=e.getDefaultGroup();if(!t||!r)throw new n(f.ErrorCodes.MergeError,"no default group");if(!t.uuid.equals(r.uuid))throw new n(f.ErrorCodes.MergeError,"default group is different");var i=this._getObjectMap();e.deletedObjects.forEach((function(e){i.deleted[e.uuid]||(this.deletedObjects.push(e),i.deleted[e.uuid]=e.deletionTime)}),this),Object.keys(e.binaries).forEach((function(t){this.binaries[t]||i.deleted[t]||(this.binaries[t]=e.binaries[t])}),this),i.remote=e._getObjectMap().objects,this.meta.merge(e.meta,i),t.merge(i),this.cleanup({historyRules:!0,customIcons:!0,binaries:!0})},y.prototype.getLocalEditState=function(){var e={};return this.getDefaultGroup().forEach((function(t){t&&t._editState&&(e[t.uuid]=t._editState)})),this.meta._editState&&(e.meta=this.meta._editState),e},y.prototype.setLocalEditState=function(e){this.getDefaultGroup().forEach((function(t){t&&e[t.uuid]&&(t._editState=e[t.uuid])})),e.meta&&(this.meta._editState=e.meta)},y.prototype.removeLocalEditState=function(){this.getDefaultGroup().forEach((function(e){e&&(e._editState=void 0)})),this.meta._editState=void 0},y.prototype.upgrade=function(){this.setVersion(a.MaxFileVersion)},y.prototype.setVersion=function(e){this.meta.headerHash=null,this.meta.settingsChanged=new Date,this.header.setVersion(e)},y.prototype.setKdf=function(e){this.meta.headerHash=null,this.meta.settingsChanged=new Date,this.header.setKdf(e)},y.prototype._getObjectMap=function(){var e={},t={};return this.getDefaultGroup().forEach((function(t,r){var i=t||r;if(e[i.uuid])throw new n(f.ErrorCodes.MergeError,"Duplicate: "+i.uuid);e[i.uuid]=i})),this.deletedObjects.forEach((function(e){t[e.uuid]=e.deletionTime})),{objects:e,deleted:t}},y.prototype._loadFromXml=function(e){if(this.xml.documentElement.tagName!==p.Elem.DocNode)throw new n(f.ErrorCodes.FileCorrupt,"bad xml root");this._parseMeta(e);var t=this;return this.binaries.hash().then((function(){return t._parseRoot(e),t}))},y.prototype._parseMeta=function(e){var t=m.getChildNode(this.xml.documentElement,p.Elem.Meta,"no meta node");this.meta=s.read(t,e)},y.prototype._parseRoot=function(e){this.groups=[],this.deletedObjects=[];for(var t=0,r=m.getChildNode(this.xml.documentElement,p.Elem.Root,"no root node").childNodes,i=r.length;t0?t.windowBits=-t.windowBits:t.gzip&&t.windowBits>0&&t.windowBits<16&&(t.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new s,this.strm.avail_out=0;var r=i.deflateInit2(this.strm,t.level,t.method,t.windowBits,t.memLevel,t.strategy);if(0!==r)throw new Error(a[r]);if(t.header&&i.deflateSetHeader(this.strm,t.header),t.dictionary){var u;if(u="string"==typeof t.dictionary?o.string2buf(t.dictionary):"[object ArrayBuffer]"===d.call(t.dictionary)?new Uint8Array(t.dictionary):t.dictionary,0!==(r=i.deflateSetDictionary(this.strm,u)))throw new Error(a[r]);this._dict_set=!0}}function u(e,t){var r=new h(t);if(r.push(e,!0),r.err)throw r.msg;return r.result}h.prototype.push=function(e,t){var r,a,s=this.strm,h=this.options.chunkSize;if(this.ended)return!1;a=t===~~t?t:!0===t?4:0,"string"==typeof e?s.input=o.string2buf(e):"[object ArrayBuffer]"===d.call(e)?s.input=new Uint8Array(e):s.input=e,s.next_in=0,s.avail_in=s.input.length;do{if(0===s.avail_out&&(s.output=new n.Buf8(h),s.next_out=0,s.avail_out=h),1!==(r=i.deflate(s,a))&&0!==r)return this.onEnd(r),this.ended=!0,!1;0!==s.avail_out&&(0!==s.avail_in||4!==a&&2!==a)||("string"===this.options.to?this.onData(o.buf2binstring(n.shrinkBuf(s.output,s.next_out))):this.onData(n.shrinkBuf(s.output,s.next_out)))}while((s.avail_in>0||0===s.avail_out)&&1!==r);return 4===a?(r=i.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,0===r):2!==a||(this.onEnd(0),s.avail_out=0,!0)},h.prototype.onData=function(e){this.chunks.push(e)},h.prototype.onEnd=function(e){0===e&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=n.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},t.Deflate=h,t.deflate=u,t.deflateRaw=function(e,t){return(t=t||{}).raw=!0,u(e,t)},t.gzip=function(e,t){return(t=t||{}).gzip=!0,u(e,t)}},function(e,t,r){"use strict";var i,n=r(5),o=r(34),a=r(17),s=r(18),d=r(12),h=-2,u=258,l=262,c=103,f=113,p=666;function m(e,t){return e.msg=d[t],t}function y(e){return(e<<1)-(e>4?9:0)}function g(e){for(var t=e.length;--t>=0;)e[t]=0}function _(e){var t=e.state,r=t.pending;r>e.avail_out&&(r=e.avail_out),0!==r&&(n.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function b(e,t){o._tr_flush_block(e,e.block_start>=0?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,_(e.strm)}function v(e,t){e.pending_buf[e.pending++]=t}function w(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function k(e,t){var r,i,n=e.max_chain_length,o=e.strstart,a=e.prev_length,s=e.nice_match,d=e.strstart>e.w_size-l?e.strstart-(e.w_size-l):0,h=e.window,c=e.w_mask,f=e.prev,p=e.strstart+u,m=h[o+a-1],y=h[o+a];e.prev_length>=e.good_match&&(n>>=2),s>e.lookahead&&(s=e.lookahead);do{if(h[(r=t)+a]===y&&h[r+a-1]===m&&h[r]===h[o]&&h[++r]===h[o+1]){o+=2,r++;do{}while(h[++o]===h[++r]&&h[++o]===h[++r]&&h[++o]===h[++r]&&h[++o]===h[++r]&&h[++o]===h[++r]&&h[++o]===h[++r]&&h[++o]===h[++r]&&h[++o]===h[++r]&&oa){if(e.match_start=t,a=i,i>=s)break;m=h[o+a-1],y=h[o+a]}}}while((t=f[t&c])>d&&0!=--n);return a<=e.lookahead?a:e.lookahead}function C(e){var t,r,i,o,d,h,u,c,f,p,m=e.w_size;do{if(o=e.window_size-e.lookahead-e.strstart,e.strstart>=m+(m-l)){n.arraySet(e.window,e.window,m,m,0),e.match_start-=m,e.strstart-=m,e.block_start-=m,t=r=e.hash_size;do{i=e.head[--t],e.head[t]=i>=m?i-m:0}while(--r);t=r=m;do{i=e.prev[--t],e.prev[t]=i>=m?i-m:0}while(--r);o+=m}if(0===e.strm.avail_in)break;if(h=e.strm,u=e.window,c=e.strstart+e.lookahead,f=o,p=void 0,(p=h.avail_in)>f&&(p=f),r=0===p?0:(h.avail_in-=p,n.arraySet(u,h.input,h.next_in,p,c),1===h.state.wrap?h.adler=a(h.adler,u,p,c):2===h.state.wrap&&(h.adler=s(h.adler,u,p,c)),h.next_in+=p,h.total_in+=p,p),e.lookahead+=r,e.lookahead+e.insert>=3)for(d=e.strstart-e.insert,e.ins_h=e.window[d],e.ins_h=(e.ins_h<=3&&(e.ins_h=(e.ins_h<=3)if(i=o._tr_tally(e,e.strstart-e.match_start,e.match_length-3),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=3){e.match_length--;do{e.strstart++,e.ins_h=(e.ins_h<=3&&(e.ins_h=(e.ins_h<4096)&&(e.match_length=2)),e.prev_length>=3&&e.match_length<=e.prev_length){n=e.strstart+e.lookahead-3,i=o._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-3),e.lookahead-=e.prev_length-1,e.prev_length-=2;do{++e.strstart<=n&&(e.ins_h=(e.ins_h<15&&(s=2,i-=16),o<1||o>9||8!==r||i<8||i>15||t<0||t>9||a<0||a>4)return m(e,h);8===i&&(i=9);var d=new T;return e.state=d,d.strm=e,d.wrap=s,d.gzhead=null,d.w_bits=i,d.w_size=1<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(C(e),0===e.lookahead&&0===t)return 1;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var i=e.block_start+r;if((0===e.strstart||e.strstart>=i)&&(e.lookahead=e.strstart-i,e.strstart=i,b(e,!1),0===e.strm.avail_out))return 1;if(e.strstart-e.block_start>=e.w_size-l&&(b(e,!1),0===e.strm.avail_out))return 1}return e.insert=0,4===t?(b(e,!0),0===e.strm.avail_out?3:4):(e.strstart>e.block_start&&(b(e,!1),e.strm.avail_out),1)})),new B(4,4,8,4,x),new B(4,5,16,8,x),new B(4,6,32,32,x),new B(4,4,16,16,E),new B(8,16,32,32,E),new B(8,16,128,128,E),new B(8,32,128,256,E),new B(32,128,258,1024,E),new B(32,258,258,4096,E)],t.deflateInit=function(e,t){return U(e,t,8,15,8,0)},t.deflateInit2=U,t.deflateReset=S,t.deflateResetKeep=A,t.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?h:(e.state.gzhead=t,0):h},t.deflate=function(e,t){var r,n,a,d;if(!e||!e.state||t>5||t<0)return e?m(e,h):h;if(n=e.state,!e.output||!e.input&&0!==e.avail_in||n.status===p&&4!==t)return m(e,0===e.avail_out?-5:h);if(n.strm=e,r=n.last_flush,n.last_flush=t,42===n.status)if(2===n.wrap)e.adler=0,v(n,31),v(n,139),v(n,8),n.gzhead?(v(n,(n.gzhead.text?1:0)+(n.gzhead.hcrc?2:0)+(n.gzhead.extra?4:0)+(n.gzhead.name?8:0)+(n.gzhead.comment?16:0)),v(n,255&n.gzhead.time),v(n,n.gzhead.time>>8&255),v(n,n.gzhead.time>>16&255),v(n,n.gzhead.time>>24&255),v(n,9===n.level?2:n.strategy>=2||n.level<2?4:0),v(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(v(n,255&n.gzhead.extra.length),v(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=s(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(v(n,0),v(n,0),v(n,0),v(n,0),v(n,0),v(n,9===n.level?2:n.strategy>=2||n.level<2?4:0),v(n,3),n.status=f);else{var l=8+(n.w_bits-8<<4)<<8;l|=(n.strategy>=2||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(l|=32),l+=31-l%31,n.status=f,w(n,l),0!==n.strstart&&(w(n,e.adler>>>16),w(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(a=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>a&&(e.adler=s(e.adler,n.pending_buf,n.pending-a,a)),_(e),a=n.pending,n.pending!==n.pending_buf_size));)v(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>a&&(e.adler=s(e.adler,n.pending_buf,n.pending-a,a)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){a=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>a&&(e.adler=s(e.adler,n.pending_buf,n.pending-a,a)),_(e),a=n.pending,n.pending===n.pending_buf_size)){d=1;break}d=n.gzindexa&&(e.adler=s(e.adler,n.pending_buf,n.pending-a,a)),0===d&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){a=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>a&&(e.adler=s(e.adler,n.pending_buf,n.pending-a,a)),_(e),a=n.pending,n.pending===n.pending_buf_size)){d=1;break}d=n.gzindexa&&(e.adler=s(e.adler,n.pending_buf,n.pending-a,a)),0===d&&(n.status=c)}else n.status=c;if(n.status===c&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&_(e),n.pending+2<=n.pending_buf_size&&(v(n,255&e.adler),v(n,e.adler>>8&255),e.adler=0,n.status=f)):n.status=f),0!==n.pending){if(_(e),0===e.avail_out)return n.last_flush=-1,0}else if(0===e.avail_in&&y(t)<=y(r)&&4!==t)return m(e,-5);if(n.status===p&&0!==e.avail_in)return m(e,-5);if(0!==e.avail_in||0!==n.lookahead||0!==t&&n.status!==p){var k=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(C(e),0===e.lookahead)){if(0===t)return 1;break}if(e.match_length=0,r=o._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(b(e,!1),0===e.strm.avail_out))return 1}return e.insert=0,4===t?(b(e,!0),0===e.strm.avail_out?3:4):e.last_lit&&(b(e,!1),0===e.strm.avail_out)?1:2}(n,t):3===n.strategy?function(e,t){for(var r,i,n,a,s=e.window;;){if(e.lookahead<=u){if(C(e),e.lookahead<=u&&0===t)return 1;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=3&&e.strstart>0&&(i=s[n=e.strstart-1])===s[++n]&&i===s[++n]&&i===s[++n]){a=e.strstart+u;do{}while(i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&ne.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=3?(r=o._tr_tally(e,1,e.match_length-3),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=o._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(b(e,!1),0===e.strm.avail_out))return 1}return e.insert=0,4===t?(b(e,!0),0===e.strm.avail_out?3:4):e.last_lit&&(b(e,!1),0===e.strm.avail_out)?1:2}(n,t):i[n.level].func(n,t);if(3!==k&&4!==k||(n.status=p),1===k||3===k)return 0===e.avail_out&&(n.last_flush=-1),0;if(2===k&&(1===t?o._tr_align(n):5!==t&&(o._tr_stored_block(n,0,0,!1),3===t&&(g(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),_(e),0===e.avail_out))return n.last_flush=-1,0}return 4!==t?0:n.wrap<=0?1:(2===n.wrap?(v(n,255&e.adler),v(n,e.adler>>8&255),v(n,e.adler>>16&255),v(n,e.adler>>24&255),v(n,255&e.total_in),v(n,e.total_in>>8&255),v(n,e.total_in>>16&255),v(n,e.total_in>>24&255)):(w(n,e.adler>>>16),w(n,65535&e.adler)),_(e),n.wrap>0&&(n.wrap=-n.wrap),0!==n.pending?0:1)},t.deflateEnd=function(e){var t;return e&&e.state?42!==(t=e.state.status)&&69!==t&&73!==t&&91!==t&&t!==c&&t!==f&&t!==p?m(e,h):(e.state=null,t===f?m(e,-3):0):h},t.deflateSetDictionary=function(e,t){var r,i,o,s,d,u,l,c,f=t.length;if(!e||!e.state)return h;if(2===(s=(r=e.state).wrap)||1===s&&42!==r.status||r.lookahead)return h;for(1===s&&(e.adler=a(e.adler,t,f,0)),r.wrap=0,f>=r.w_size&&(0===s&&(g(r.head),r.strstart=0,r.block_start=0,r.insert=0),c=new n.Buf8(r.w_size),n.arraySet(c,t,f-r.w_size,r.w_size,0),t=c,f=r.w_size),d=e.avail_in,u=e.next_in,l=e.input,e.avail_in=f,e.next_in=0,e.input=t,C(r);r.lookahead>=3;){i=r.strstart,o=r.lookahead-2;do{r.ins_h=(r.ins_h<=0;)e[t]=0}var o=256,a=286,s=30,d=15,h=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],u=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],l=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],c=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],f=new Array(576);n(f);var p=new Array(60);n(p);var m=new Array(512);n(m);var y=new Array(256);n(y);var g=new Array(29);n(g);var _,b,v,w=new Array(s);function k(e,t,r,i,n){this.static_tree=e,this.extra_bits=t,this.extra_base=r,this.elems=i,this.max_length=n,this.has_stree=e&&e.length}function C(e,t){this.dyn_tree=e,this.max_code=0,this.stat_desc=t}function x(e){return e<256?m[e]:m[256+(e>>>7)]}function E(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function B(e,t,r){e.bi_valid>16-r?(e.bi_buf|=t<>16-e.bi_valid,e.bi_valid+=r-16):(e.bi_buf|=t<>>=1,r<<=1}while(--t>0);return r>>>1}function S(e,t,r){var i,n,o=new Array(16),a=0;for(i=1;i<=d;i++)o[i]=a=a+r[i-1]<<1;for(n=0;n<=t;n++){var s=e[2*n+1];0!==s&&(e[2*n]=A(o[s]++,s))}}function U(e){var t;for(t=0;t8?E(e,e.bi_buf):e.bi_valid>0&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0}function I(e,t,r,i){var n=2*t,o=2*r;return e[n]>1;r>=1;r--)N(e,o,r);n=h;do{r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],N(e,o,1),i=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=i,o[2*n]=o[2*r]+o[2*i],e.depth[n]=(e.depth[r]>=e.depth[i]?e.depth[r]:e.depth[i])+1,o[2*r+1]=o[2*i+1]=n,e.heap[1]=n++,N(e,o,1)}while(e.heap_len>=2);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,i,n,o,a,s,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,c=t.stat_desc.has_stree,f=t.stat_desc.extra_bits,p=t.stat_desc.extra_base,m=t.stat_desc.max_length,y=0;for(o=0;o<=d;o++)e.bl_count[o]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<573;r++)(o=h[2*h[2*(i=e.heap[r])+1]+1]+1)>m&&(o=m,y++),h[2*i+1]=o,i>u||(e.bl_count[o]++,a=0,i>=p&&(a=f[i-p]),s=h[2*i],e.opt_len+=s*(o+a),c&&(e.static_len+=s*(l[2*i+1]+a)));if(0!==y){do{for(o=m-1;0===e.bl_count[o];)o--;e.bl_count[o]--,e.bl_count[o+1]+=2,e.bl_count[m]--,y-=2}while(y>0);for(o=m;0!==o;o--)for(i=e.bl_count[o];0!==i;)(n=e.heap[--r])>u||(h[2*n+1]!==o&&(e.opt_len+=(o-h[2*n+1])*h[2*n],h[2*n+1]=o),i--)}}(e,t),S(o,u,e.bl_count)}function F(e,t,r){var i,n,o=-1,a=t[1],s=0,d=7,h=4;for(0===a&&(d=138,h=3),t[2*(r+1)+1]=65535,i=0;i<=r;i++)n=a,a=t[2*(i+1)+1],++s>=7;i0?(2===e.strm.data_type&&(e.strm.data_type=function(e){var t,r=4093624447;for(t=0;t<=31;t++,r>>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return 0;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return 1;for(t=32;t=3&&0===e.bl_tree[2*c[t]+1];t--);return e.opt_len+=3*(t+1)+5+5+4,t}(e),n=e.opt_len+3+7>>>3,(a=e.static_len+3+7>>>3)<=n&&(n=a)):n=a=r+5,r+4<=n&&-1!==t?V(e,t,r,i):4===e.strategy||a===n?(B(e,2+(i?1:0),3),P(e,f,p)):(B(e,4+(i?1:0),3),function(e,t,r,i){var n;for(B(e,t-257,5),B(e,r-1,5),B(e,i-4,4),n=0;n>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(y[r]+o+1)]++,e.dyn_dtree[2*x(t)]++),e.last_lit===e.lit_bufsize-1},t._tr_align=function(e){B(e,2,3),T(e,256,f),function(e){16===e.bi_valid?(E(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):e.bi_valid>=8&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},function(e,t,r){"use strict";var i=r(36),n=r(5),o=r(19),a=r(21),s=r(12),d=r(20),h=r(39),u=Object.prototype.toString;function l(e){if(!(this instanceof l))return new l(e);this.options=n.assign({chunkSize:16384,windowBits:0,to:""},e||{});var t=this.options;t.raw&&t.windowBits>=0&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(t.windowBits>=0&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),t.windowBits>15&&t.windowBits<48&&0==(15&t.windowBits)&&(t.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new d,this.strm.avail_out=0;var r=i.inflateInit2(this.strm,t.windowBits);if(r!==a.Z_OK)throw new Error(s[r]);this.header=new h,i.inflateGetHeader(this.strm,this.header)}function c(e,t){var r=new l(t);if(r.push(e,!0),r.err)throw r.msg;return r.result}l.prototype.push=function(e,t){var r,s,d,h,l,c,f=this.strm,p=this.options.chunkSize,m=this.options.dictionary,y=!1;if(this.ended)return!1;s=t===~~t?t:!0===t?a.Z_FINISH:a.Z_NO_FLUSH,"string"==typeof e?f.input=o.binstring2buf(e):"[object ArrayBuffer]"===u.call(e)?f.input=new Uint8Array(e):f.input=e,f.next_in=0,f.avail_in=f.input.length;do{if(0===f.avail_out&&(f.output=new n.Buf8(p),f.next_out=0,f.avail_out=p),(r=i.inflate(f,a.Z_NO_FLUSH))===a.Z_NEED_DICT&&m&&(c="string"==typeof m?o.string2buf(m):"[object ArrayBuffer]"===u.call(m)?new Uint8Array(m):m,r=i.inflateSetDictionary(this.strm,c)),r===a.Z_BUF_ERROR&&!0===y&&(r=a.Z_OK,y=!1),r!==a.Z_STREAM_END&&r!==a.Z_OK)return this.onEnd(r),this.ended=!0,!1;f.next_out&&(0!==f.avail_out&&r!==a.Z_STREAM_END&&(0!==f.avail_in||s!==a.Z_FINISH&&s!==a.Z_SYNC_FLUSH)||("string"===this.options.to?(d=o.utf8border(f.output,f.next_out),h=f.next_out-d,l=o.buf2string(f.output,d),f.next_out=h,f.avail_out=p-h,h&&n.arraySet(f.output,f.output,d,h,0),this.onData(l)):this.onData(n.shrinkBuf(f.output,f.next_out)))),0===f.avail_in&&0===f.avail_out&&(y=!0)}while((f.avail_in>0||0===f.avail_out)&&r!==a.Z_STREAM_END);return r===a.Z_STREAM_END&&(s=a.Z_FINISH),s===a.Z_FINISH?(r=i.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===a.Z_OK):s!==a.Z_SYNC_FLUSH||(this.onEnd(a.Z_OK),f.avail_out=0,!0)},l.prototype.onData=function(e){this.chunks.push(e)},l.prototype.onEnd=function(e){e===a.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=n.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},t.Inflate=l,t.inflate=c,t.inflateRaw=function(e,t){return(t=t||{}).raw=!0,c(e,t)},t.ungzip=c},function(e,t,r){"use strict";var i=r(5),n=r(17),o=r(18),a=r(37),s=r(38),d=-2,h=12,u=30;function l(e){return(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function c(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new i.Buf16(320),this.work=new i.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function f(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=1,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new i.Buf32(852),t.distcode=t.distdyn=new i.Buf32(592),t.sane=1,t.back=-1,0):d}function p(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,f(e)):d}function m(e,t){var r,i;return e&&e.state?(i=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||t>15)?d:(null!==i.window&&i.wbits!==t&&(i.window=null),i.wrap=r,i.wbits=t,p(e))):d}function y(e,t){var r,i;return e?(i=new c,e.state=i,i.window=null,0!==(r=m(e,t))&&(e.state=null),r):d}var g,_,b=!0;function v(e){if(b){var t;for(g=new i.Buf32(512),_=new i.Buf32(32),t=0;t<144;)e.lens[t++]=8;for(;t<256;)e.lens[t++]=9;for(;t<280;)e.lens[t++]=7;for(;t<288;)e.lens[t++]=8;for(s(1,e.lens,0,288,g,0,e.work,{bits:9}),t=0;t<32;)e.lens[t++]=5;s(2,e.lens,0,32,_,0,e.work,{bits:5}),b=!1}e.lencode=g,e.lenbits=9,e.distcode=_,e.distbits=5}function w(e,t,r,n){var o,a=e.state;return null===a.window&&(a.wsize=1<=a.wsize?(i.arraySet(a.window,t,r-a.wsize,a.wsize,0),a.wnext=0,a.whave=a.wsize):((o=a.wsize-a.wnext)>n&&(o=n),i.arraySet(a.window,t,r-n,o,a.wnext),(n-=o)?(i.arraySet(a.window,t,r-n,n,0),a.wnext=n,a.whave=a.wsize):(a.wnext+=o,a.wnext===a.wsize&&(a.wnext=0),a.whave>>8&255,r.check=o(r.check,O,2,0),_=0,b=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&_)<<8)+(_>>8))%31){e.msg="incorrect header check",r.mode=u;break}if(8!=(15&_)){e.msg="unknown compression method",r.mode=u;break}if(b-=4,N=8+(15&(_>>>=4)),0===r.wbits)r.wbits=N;else if(N>r.wbits){e.msg="invalid window size",r.mode=u;break}r.dmax=1<>8&1),512&r.flags&&(O[0]=255&_,O[1]=_>>>8&255,r.check=o(r.check,O,2,0)),_=0,b=0,r.mode=3;case 3:for(;b<32;){if(0===y)break e;y--,_+=c[p++]<>>8&255,O[2]=_>>>16&255,O[3]=_>>>24&255,r.check=o(r.check,O,4,0)),_=0,b=0,r.mode=4;case 4:for(;b<16;){if(0===y)break e;y--,_+=c[p++]<>8),512&r.flags&&(O[0]=255&_,O[1]=_>>>8&255,r.check=o(r.check,O,2,0)),_=0,b=0,r.mode=5;case 5:if(1024&r.flags){for(;b<16;){if(0===y)break e;y--,_+=c[p++]<>>8&255,r.check=o(r.check,O,2,0)),_=0,b=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&((x=r.length)>y&&(x=y),x&&(r.head&&(N=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),i.arraySet(r.head.extra,c,p,x,N)),512&r.flags&&(r.check=o(r.check,c,x,p)),y-=x,p+=x,r.length-=x),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===y)break e;x=0;do{N=c[p+x++],r.head&&N&&r.length<65536&&(r.head.name+=String.fromCharCode(N))}while(N&&x>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=h;break;case 10:for(;b<32;){if(0===y)break e;y--,_+=c[p++]<>>=7&b,b-=7&b,r.mode=27;break}for(;b<3;){if(0===y)break e;y--,_+=c[p++]<>>=1)){case 0:r.mode=14;break;case 1:if(v(r),r.mode=20,6===t){_>>>=2,b-=2;break e}break;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=u}_>>>=2,b-=2;break;case 14:for(_>>>=7&b,b-=7&b;b<32;){if(0===y)break e;y--,_+=c[p++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=u;break}if(r.length=65535&_,_=0,b=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(x=r.length){if(x>y&&(x=y),x>g&&(x=g),0===x)break e;i.arraySet(f,c,p,x,m),y-=x,p+=x,g-=x,m+=x,r.length-=x;break}r.mode=h;break;case 17:for(;b<14;){if(0===y)break e;y--,_+=c[p++]<>>=5,b-=5,r.ndist=1+(31&_),_>>>=5,b-=5,r.ncode=4+(15&_),_>>>=4,b-=4,r.nlen>286||r.ndist>30){e.msg="too many length or distance symbols",r.mode=u;break}r.have=0,r.mode=18;case 18:for(;r.have>>=3,b-=3}for(;r.have<19;)r.lens[V[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,M={bits:r.lenbits},P=s(0,r.lens,0,19,r.lencode,0,r.work,M),r.lenbits=M.bits,P){e.msg="invalid code lengths set",r.mode=u;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,S=65535&z,!((T=z>>>24)<=b);){if(0===y)break e;y--,_+=c[p++]<>>=T,b-=T,r.lens[r.have++]=S;else{if(16===S){for(F=T+2;b>>=T,b-=T,0===r.have){e.msg="invalid bit length repeat",r.mode=u;break}N=r.lens[r.have-1],x=3+(3&_),_>>>=2,b-=2}else if(17===S){for(F=T+3;b>>=T)),_>>>=3,b-=3}else{for(F=T+7;b>>=T)),_>>>=7,b-=7}if(r.have+x>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=u;break}for(;x--;)r.lens[r.have++]=N}}if(r.mode===u)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=u;break}if(r.lenbits=9,M={bits:r.lenbits},P=s(1,r.lens,0,r.nlen,r.lencode,0,r.work,M),r.lenbits=M.bits,P){e.msg="invalid literal/lengths set",r.mode=u;break}if(r.distbits=6,r.distcode=r.distdyn,M={bits:r.distbits},P=s(2,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,M),r.distbits=M.bits,P){e.msg="invalid distances set",r.mode=u;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(y>=6&&g>=258){e.next_out=m,e.avail_out=g,e.next_in=p,e.avail_in=y,r.hold=_,r.bits=b,a(e,C),m=e.next_out,f=e.output,g=e.avail_out,p=e.next_in,c=e.input,y=e.avail_in,_=r.hold,b=r.bits,r.mode===h&&(r.back=-1);break}for(r.back=0;A=(z=r.lencode[_&(1<>>16&255,S=65535&z,!((T=z>>>24)<=b);){if(0===y)break e;y--,_+=c[p++]<>U)])>>>16&255,S=65535&z,!(U+(T=z>>>24)<=b);){if(0===y)break e;y--,_+=c[p++]<>>=U,b-=U,r.back+=U}if(_>>>=T,b-=T,r.back+=T,r.length=S,0===A){r.mode=26;break}if(32&A){r.back=-1,r.mode=h;break}if(64&A){e.msg="invalid literal/length code",r.mode=u;break}r.extra=15&A,r.mode=22;case 22:if(r.extra){for(F=r.extra;b>>=r.extra,b-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;A=(z=r.distcode[_&(1<>>16&255,S=65535&z,!((T=z>>>24)<=b);){if(0===y)break e;y--,_+=c[p++]<>U)])>>>16&255,S=65535&z,!(U+(T=z>>>24)<=b);){if(0===y)break e;y--,_+=c[p++]<>>=U,b-=U,r.back+=U}if(_>>>=T,b-=T,r.back+=T,64&A){e.msg="invalid distance code",r.mode=u;break}r.offset=S,r.extra=15&A,r.mode=24;case 24:if(r.extra){for(F=r.extra;b>>=r.extra,b-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=u;break}r.mode=25;case 25:if(0===g)break e;if(x=C-g,r.offset>x){if((x=r.offset-x)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=u;break}x>r.wnext?(x-=r.wnext,E=r.wsize-x):E=r.wnext-x,x>r.length&&(x=r.length),B=r.window}else B=f,E=m-r.offset,x=r.length;x>g&&(x=g),g-=x,r.length-=x;do{f[m++]=B[E++]}while(--x);0===r.length&&(r.mode=21);break;case 26:if(0===g)break e;f[m++]=r.length,g--,r.mode=21;break;case 27:if(r.wrap){for(;b<32;){if(0===y)break e;y--,_|=c[p++]<>>=v=b>>>24,p-=v,0===(v=b>>>16&255))B[o++]=65535&b;else{if(!(16&v)){if(0==(64&v)){b=m[(65535&b)+(f&(1<>>=v,p-=v),p<15&&(f+=E[i++]<>>=v=b>>>24,p-=v,!(16&(v=b>>>16&255))){if(0==(64&v)){b=y[(65535&b)+(f&(1<d){e.msg="invalid distance too far back",r.mode=30;break e}if(f>>>=v,p-=v,k>(v=o-a)){if((v=k-v)>u&&r.sane){e.msg="invalid distance too far back",r.mode=30;break e}if(C=0,x=c,0===l){if(C+=h-v,v2;)B[o++]=x[C++],B[o++]=x[C++],B[o++]=x[C++],w-=3;w&&(B[o++]=x[C++],w>1&&(B[o++]=x[C++]))}else{C=o-k;do{B[o++]=B[C++],B[o++]=B[C++],B[o++]=B[C++],w-=3}while(w>2);w&&(B[o++]=B[C++],w>1&&(B[o++]=B[C++]))}break}}break}}while(i>3,f&=(1<<(p-=w<<3))-1,e.next_in=i,e.next_out=o,e.avail_in=i=1&&0===F[T];T--);if(A>T&&(A=T),0===T)return u[l++]=20971520,u[l++]=20971520,f.bits=1,0;for(B=1;B0&&(0===e||1!==T))return-1;for(z[1]=0,x=1;x852||2===e&&I>592)return 1;for(;;){v=x-U,c[E]b?(w=O[V+c[E]],k=P[M+c[E]]):(w=96,k=0),p=1<>U)+(m-=p)]=v<<24|w<<16|k|0}while(0!==m);for(p=1<>=1;if(0!==p?(N&=p-1,N+=p):N=0,E++,0==--F[x]){if(x===T)break;x=t[r+c[E]]}if(x>A&&(N&g)!==y){for(0===U&&(U=A),_+=B,D=1<<(S=x-U);S+U852||2===e&&I>592)return 1;u[y=N&g]=A<<24|S<<16|_-l|0}}return 0!==N&&(u[_+N]=x-U<<24|64<<16|0),f.bits=A,0}},function(e,t,r){"use strict";e.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},function(e,t,r){var i=r(41);e.exports={TextEncoder:i.TextEncoder,TextDecoder:i.TextDecoder}},function(e,t,r){!function(t){"use strict";function r(e,t,r){return t<=e&&e<=r}function i(e){if(void 0===e)return{};if(e===Object(e))return e;throw TypeError("Could not convert argument to dictionary")}var n=-1;function o(e){this.tokens=[].slice.call(e),this.tokens.reverse()}o.prototype={endOfStream:function(){return!this.tokens.length},read:function(){return this.tokens.length?this.tokens.pop():n},prepend:function(e){if(Array.isArray(e))for(var t=e;t.length;)this.tokens.push(t.pop());else this.tokens.push(e)},push:function(e){if(Array.isArray(e))for(var t=e;t.length;)this.tokens.unshift(t.shift());else this.tokens.unshift(e)}};var a=-1;function s(e,t){if(e)throw TypeError("Decoder error");return t||65533}function d(e){return e=String(e).trim().toLowerCase(),Object.prototype.hasOwnProperty.call(h,e)?h[e]:null}var h={};[{encodings:[{labels:["unicode-1-1-utf-8","utf-8","utf8"],name:"UTF-8"}],heading:"The Encoding"}].forEach((function(e){e.encodings.forEach((function(e){e.labels.forEach((function(t){h[t]=e}))}))}));var u={},l={};function c(e,t){if(!(this instanceof c))throw TypeError("Called as a function. Did you forget 'new'?");e=void 0!==e?String(e):"utf-8",t=i(t),this._encoding=null,this._decoder=null,this._ignoreBOM=!1,this._BOMseen=!1,this._error_mode="replacement",this._do_not_flush=!1;var r=d(e);if(null===r||"replacement"===r.name)throw RangeError("Unknown encoding: "+e);if(!l[r.name])throw Error("Decoder not present. Did you forget to include encoding-indexes.js?");var n=this;return n._encoding=r,Boolean(t.fatal)&&(n._error_mode="fatal"),Boolean(t.ignoreBOM)&&(n._ignoreBOM=!0),n}function f(e,r){if(!(this instanceof f))throw TypeError("Called as a function. Did you forget 'new'?");r=i(r),this._encoding=null,this._encoder=null,this._do_not_flush=!1,this._fatal=Boolean(r.fatal)?"fatal":"replacement";return this._encoding=d("utf-8"),void 0!==e&&"console"in t&&console.warn("TextEncoder constructor called with encoding label, which is ignored."),this}function p(e){var t=e.fatal,i=0,o=0,d=0,h=128,u=191;this.handler=function(e,l){if(l===n&&0!==d)return d=0,s(t);if(l===n)return a;if(0===d){if(r(l,0,127))return l;if(r(l,194,223))d=1,i=31&l;else if(r(l,224,239))224===l&&(h=160),237===l&&(u=159),d=2,i=15&l;else{if(!r(l,240,244))return s(t);240===l&&(h=144),244===l&&(u=143),d=3,i=7&l}return null}if(!r(l,h,u))return i=d=o=0,h=128,u=191,e.prepend(l),s(t);if(h=128,u=191,i=i<<6|63&l,(o+=1)!==d)return null;var c=i;return i=d=o=0,c}}function m(e){e.fatal;this.handler=function(e,t){if(t===n)return a;if(r(t,0,127))return t;var i,o;r(t,128,2047)?(i=1,o=192):r(t,2048,65535)?(i=2,o=224):r(t,65536,1114111)&&(i=3,o=240);for(var s=[(t>>6*i)+o];i>0;){var d=t>>6*(i-1);s.push(128|63&d),i-=1}return s}}Object.defineProperty&&(Object.defineProperty(c.prototype,"encoding",{get:function(){return this._encoding.name.toLowerCase()}}),Object.defineProperty(c.prototype,"fatal",{get:function(){return"fatal"===this._error_mode}}),Object.defineProperty(c.prototype,"ignoreBOM",{get:function(){return this._ignoreBOM}})),c.prototype.decode=function(e,t){var r;r="object"==typeof e&&e instanceof ArrayBuffer?new Uint8Array(e):"object"==typeof e&&"buffer"in e&&e.buffer instanceof ArrayBuffer?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(0),t=i(t),this._do_not_flush||(this._decoder=l[this._encoding.name]({fatal:"fatal"===this._error_mode}),this._BOMseen=!1),this._do_not_flush=Boolean(t.stream);for(var s,d=new o(r),h=[];;){var u=d.read();if(u===n)break;if((s=this._decoder.handler(d,u))===a)break;null!==s&&(Array.isArray(s)?h.push.apply(h,s):h.push(s))}if(!this._do_not_flush){do{if((s=this._decoder.handler(d,d.read()))===a)break;null!==s&&(Array.isArray(s)?h.push.apply(h,s):h.push(s))}while(!d.endOfStream());this._decoder=null}return function(e){var t,r;return t=["UTF-8","UTF-16LE","UTF-16BE"],r=this._encoding.name,-1===t.indexOf(r)||this._ignoreBOM||this._BOMseen||(e.length>0&&65279===e[0]?(this._BOMseen=!0,e.shift()):e.length>0&&(this._BOMseen=!0)),function(e){for(var t="",r=0;r>10),56320+(1023&i)))}return t}(e)}.call(this,h)},Object.defineProperty&&Object.defineProperty(f.prototype,"encoding",{get:function(){return this._encoding.name.toLowerCase()}}),f.prototype.encode=function(e,t){e=e?String(e):"",t=i(t),this._do_not_flush||(this._encoder=u[this._encoding.name]({fatal:"fatal"===this._fatal})),this._do_not_flush=Boolean(t.stream);for(var r,s=new o(function(e){for(var t=String(e),r=t.length,i=0,n=[];i57343)n.push(o);else if(56320<=o&&o<=57343)n.push(65533);else if(55296<=o&&o<=56319)if(i===r-1)n.push(65533);else{var a=t.charCodeAt(i+1);if(56320<=a&&a<=57343){var s=1023&o,d=1023&a;n.push(65536+(s<<10)+d),i+=1}else n.push(65533)}i+=1}return n}(e)),d=[];;){var h=s.read();if(h===n)break;if((r=this._encoder.handler(s,h))===a)break;Array.isArray(r)?d.push.apply(d,r):d.push(r)}if(!this._do_not_flush){for(;(r=this._encoder.handler(s,s.read()))!==a;)Array.isArray(r)?d.push.apply(d,r):d.push(r);this._encoder=null}return new Uint8Array(d)},u["UTF-8"]=function(e){return new m(e)},l["UTF-8"]=function(e){return new p(e)},t.TextEncoder||(t.TextEncoder=f),t.TextDecoder||(t.TextDecoder=c),e.exports&&(e.exports={TextEncoder:t.TextEncoder,TextDecoder:t.TextDecoder})}(this)},function(t,r){t.exports=e},function(e,t,r){"use strict";var i=r(4),n=function(e){this.kdbx=e.kdbx,this.exportXml=e.exportXml||!1};n.prototype.setXmlDate=function(e,t){var r=this.kdbx.header.versionMajor>=4&&!this.exportXml;i.setDate(e,t,r)},e.exports=n},function(e,r){e.exports=t},function(e,t,r){"use strict";var i=r(11),n=r(2),o=r(1),a=r(0),s=r(3);e.exports.decrypt=function(e){return Promise.resolve().then((function(){var t,r=new i(e),d=[],h=0,u=0,l=function(){if(r.getUint32(!0),t=r.readBytes(32),(h=r.getUint32(!0))>0){u+=h;var e=r.readBytes(h);return s.sha256(e).then((function(r){if(a.arrayBufferEquals(r,t))return d.push(e),l();throw new n(o.ErrorCodes.FileCorrupt,"invalid hash block")}))}for(var i=new Uint8Array(u),c=0,f=0;f0){var h=Math.min(1048576,t);t-=h;var u=e.slice(r,r+h);return s.sha256(u).then((function(e){var t=new ArrayBuffer(40),s=new i(t);return s.setUint32(n,!0),s.writeBytes(e),s.setUint32(h,!0),a.push(t),o+=t.byteLength,a.push(u),o+=u.byteLength,n++,r+=h,d()}))}var l=new ArrayBuffer(40);new DataView(l).setUint32(0,n,!0),a.push(l),o+=l.byteLength;for(var c=new Uint8Array(o),f=0,p=0;p0){h+=d;var c=r.readBytes(d);return u(t,s,d,c).then((function(t){if(a.arrayBufferEquals(t,e))return i.push(c),s++,l();throw new n(o.ErrorCodes.FileCorrupt,"invalid hash block")}))}for(var f=new Uint8Array(h),p=0,m=0;m0)return a.push(l),o+=l.byteLength,n++,i+=h,d();for(var u=new Uint8Array(o),c=0,f=0;fthis.nameChanged&&(this._name=e.name,this.nameChanged=e.nameChanged),e.descChanged>this.descChanged&&(this._desc=e.desc,this.descChanged=e.descChanged),e.defaultUserChanged>this.defaultUserChanged&&(this._defaultUser=e.defaultUser,this.defaultUserChanged=e.defaultUserChanged),e.keyChanged>this.keyChanged&&(this.keyChanged=e.keyChanged),e.settingsChanged>this.settingsChanged&&(this.settingsChanged=e.settingsChanged),e.recycleBinChanged>this.recycleBinChanged&&(this._recycleBinEnabled=e.recycleBinEnabled,this._recycleBinUuid=e.recycleBinUuid,this.recycleBinChanged=e.recycleBinChanged),e.entryTemplatesGroupChanged>this.entryTemplatesGroupChanged&&(this._entryTemplatesGroup=e.entryTemplatesGroup,this.entryTemplatesGroupChanged=e.entryTemplatesGroupChanged),Object.keys(e.customData).forEach((function(r){this.customData[r]||t.deleted[r]||(this.customData[r]=e.customData[r])}),this),Object.keys(e.customIcons).forEach((function(r){this.customIcons[r]||t.deleted[r]||(this.customIcons[r]=e.customIcons[r])}),this),this._editState&&this._editState.historyMaxItems||(this.historyMaxItems=e.historyMaxItems),this._editState&&this._editState.historyMaxSize||(this.historyMaxSize=e.historyMaxSize),this._editState&&this._editState.keyChangeRec||(this.keyChangeRec=e.keyChangeRec),this._editState&&this._editState.keyChangeForce||(this.keyChangeForce=e.keyChangeForce),this._editState&&this._editState.mntncHistoryDays||(this.mntncHistoryDays=e.mntncHistoryDays),this._editState&&this._editState.color||(this.color=e.color)},h.create=function(){var e=new Date,t=new h;return t.generator=d.Generator,t.settingsChanged=e,t.mntncHistoryDays=s.Defaults.MntncHistoryDays,t.recycleBinEnabled=!0,t.historyMaxItems=s.Defaults.HistoryMaxItems,t.historyMaxSize=s.Defaults.HistoryMaxSize,t.nameChanged=e,t.descChanged=e,t.defaultUserChanged=e,t.recycleBinChanged=e,t.keyChangeRec=-1,t.keyChangeForce=-1,t.entryTemplatesGroup=new n,t.entryTemplatesGroupChanged=e,t.memoryProtection={title:!1,userName:!1,password:!0,url:!1,notes:!1},t},h.read=function(e,t){for(var r=new h,i=0,n=e.childNodes,o=n.length;ithis.times.lastModTime&&this.copyFrom(t),this.groups=this._mergeCollection(this.groups,t.groups,e),this.entries=this._mergeCollection(this.entries,t.entries,e),this.groups.forEach((function(t){t.merge(e)})),this.entries.forEach((function(t){t.merge(e)})))},u.prototype._mergeCollection=function(e,t,r){var i=[];return e.forEach((function(e){if(!r.deleted[e.uuid]){var t=r.remote[e.uuid];t?t.times.locationChanged<=e.times.locationChanged&&i.push(e):i.push(e)}}),this),t.forEach((function(e,n){if(!r.deleted[e.uuid]){var o=r.objects[e.uuid];if(o&&e.times.locationChanged>o.times.locationChanged)o.parentGroup=this,i.splice(this._findInsertIx(i,t,n),0,o);else if(!o){var a=new e.constructor;a.copyFrom(e),a.parentGroup=this,i.splice(this._findInsertIx(i,t,n),0,a)}}}),this),i},u.prototype._findInsertIx=function(e,t,r){for(var i=e.length,n=-1,o=0;o<=e.length;o++){var a=0,s=r>0?t[r-1].uuid.id:void 0,d=r+10?e[o-1].uuid.id:void 0,u=on&&(i=o,n=a)}return i},u.prototype.copyFrom=function(e){this.uuid=e.uuid,this.name=e.name,this.notes=e.notes,this.icon=e.icon,this.customIcon=e.customIcon,this.times=e.times.clone(),this.expanded=e.expanded,this.defaultAutoTypeSeq=e.defaultAutoTypeSeq,this.enableAutoType=e.enableAutoType,this.enableSearching=e.enableSearching,this.lastTopVisibleEntry=e.lastTopVisibleEntry},u.create=function(e,t){var r=new u;return r.uuid=d.random(),r.icon=o.Icons.Folder,r.times=s.create(),r.name=e,r.parentGroup=t,r.expanded=!0,r.enableAutoType=null,r.enableSearching=null,r.lastTopVisibleEntry=new d,r},u.read=function(e,t,r){for(var i=new u,n=0,o=e.childNodes,a=o.length;n { + var open = indexedDB.open(Database, 1); + open.onupgradeneeded = function () { + var db = open.result; + db.createObjectStore(Database, {keyPath: "id"}); + }; + + open.onerror = function (event) { + reject(`error opening database ${event.target.errorCode}`); + }; + + open.onsuccess = function () { + var db = open.result; + var tx = db.transaction(Database, "readwrite"); + + resolve(tx.objectStore(Database)); + + tx.oncomplete = function () { + db.close(); + }; + }; + }); + }, + + /** + * Open the object store and extract the keys using the id + * + * @private + * @param {String} uuid + * @returns the result from the object store or false + */ + _get_keys: async function (uuid) { + var self = this; + return new Promise((resolve, reject) => { + self._get_object_store().then((store) => { + const request = store.get(uuid); + request.onerror = function (event) { + reject(`error opening database ${event.target.errorCode}`); + }; + request.onsuccess = function () { + resolve(request.result); + }; + }); + }); + }, + + /** + * Check if the keys exist in the database + * + * @returns the uuid of the currently active keys or false + */ + _check_database: async function () { + const params = await this.rpc("/vault/keys/get"); + if (Object.keys(params).length && params.uuid) return params.uuid; + return false; + }, + + /** + * Check if the keys exist in the store + * + * @private + * @param {String} uuid + * @returns if the keys are in the object store + */ + _check_store: async function (uuid) { + if (!uuid) return false; + + const result = await this._get_keys(uuid); + return Boolean(result && result.keys); + }, + + /** + * Import the keys from the indexed DB + * + * @private + * @returns if the import from the object store succeeded + */ + _import_from_store: async function () { + const data = await this._get_keys(this.uuid); + if (data) { + this.keys = data.keys; + this.time = data.time; + return true; + } + return false; + }, + + /** + * Export the current keys to the indexed DB + * + * @private + * @returns true + */ + _export_to_store: async function () { + const keys = {id: this.uuid, keys: this.keys, time: this.time}; + const store = await this._get_object_store(); + store.put(keys); + return true; + }, + + /** + * Export the key pairs to the backends + * + * @private + * @returns if the export to the database succeeded + */ + _export_to_database: async function () { + // Generate salt for the user key + this.salt = utils.generate_bytes(utils.SaltLength).buffer; + this.iterations = 4000; + + // Wrap the private key with the master key of the user + this.iv = utils.generate_bytes(utils.IVLength); + + // Request the password from the user and derive the user key + const pass = await utils.derive_key( + await askpassword(true), + this.salt, + this.iterations + ); + + // Export the private key wrapped with the master key + const private_key = await utils.export_private_key( + await this.get_private_key(), + pass, + this.iv + ); + + // Export the public key + const public_key = await utils.export_public_key( + await this.get_public_key() + ); + + const params = { + public: public_key, + private: private_key, + iv: utils.toBase64(this.iv), + iterations: this.iterations, + salt: utils.toBase64(this.salt), + }; + + // Export to the server + const response = await this.rpc("/vault/keys/store", params); + if (response) { + this.uuid = response; + return true; + } + + console.error("Failed to export keys to database"); + return false; + }, + + /** + * Import the keys from the backend and decrypt the private key + * + * @private + * @returns if the import succeeded + */ + _import_from_database: async function () { + const params = await this.rpc("/vault/keys/get"); + if (Object.keys(params).length) { + this.salt = utils.fromBase64(params.salt); + this.iterations = params.iterations; + + // Request the password from the user and derive the user key + const pass = await utils.derive_key( + await askpassword(), + this.salt, + this.iterations + ); + + this.keys = { + publicKey: await utils.load_public_key(params.public), + privateKey: await utils.load_private_key( + params.private, + pass, + params.iv + ), + }; + + this.time = new Date(); + this.uuid = params.uuid; + return true; + } + return false; + }, + + /** + * Wrap the master key with the own public key + * + * @param {CryptoKey} master_key + * @returns wrapped master key + */ + wrap: async function (master_key) { + return await utils.wrap(master_key, await this.get_public_key()); + }, + + /** + * Wrap the master key with a public key given as string + * + * @param {CryptoKey} master_key + * @param {String} public_key + * @returns wrapped master key + */ + wrap_with: async function (master_key, public_key) { + const pub_key = await utils.load_public_key(public_key); + return await utils.wrap(master_key, pub_key); + }, + + /** + * Unwrap the master key with the own private key + * + * @param {CryptoKey} master_key + * @returns unwrapped master key + */ + unwrap: async function (master_key) { + return await utils.unwrap(master_key, await this.get_private_key()); + }, + + /** + * Share a wrapped master key by unwrapping with own private key and wrapping with + * another key + * + * @param {String} master_key + * @param {String} public_key + * @returns wrapped master key + */ + share: async function (master_key, public_key) { + const key = await this.unwrap(master_key); + return await this.wrap_with(key, public_key); + }, + }); + + return new Vault(); +}); diff --git a/vault/static/src/js/vault_controller.js b/vault/static/src/js/vault_controller.js new file mode 100644 index 0000000000..4df1dc520e --- /dev/null +++ b/vault/static/src/js/vault_controller.js @@ -0,0 +1,350 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +odoo.define("vault.controller", function (require) { + "use strict"; + + var core = require("web.core"); + var Dialog = require("web.Dialog"); + var FormController = require("web.FormController"); + var Importer = require("vault.import"); + var utils = require("vault.utils"); + var vault = require("vault"); + + var _t = core._t; + + FormController.include({ + /** + * Re-encrypt the key if the user is getting selected + * + * @private + * @param {Object} record + * @param {Object} changes + * @param {Object} options + */ + _applyChangesSendWizard: async function (record, changes, options) { + if (!changes.user_id || !record.data.public) return; + + const key = await vault.unwrap(record.data.key); + await this._applyChanges( + record.id, + {key_user: await vault.wrap_with(key, record.data.public)}, + options + ); + }, + + /** + * Re-encrypt the key if the entry is getting selected + * + * @private + * @param {Object} record + * @param {Object} changes + * @param {Object} options + */ + _applyChangesStoreWizard: async function (record, changes, options) { + if ( + !changes.entry_id || + !record.data.master_key || + !record.data.iv || + !record.data.secret_temporary + ) + return; + + const key = await vault.unwrap(record.data.key); + const secret = await utils.sym_decrypt( + key, + record.data.secret_temporary, + record.data.iv + ); + const master_key = await vault.unwrap(record.data.master_key); + + await this._applyChanges( + record.id, + {secret: await utils.sym_encrypt(master_key, secret, record.data.iv)}, + options + ); + }, + + /** + * Generate a new key pair for the current user + * + * @private + */ + _newVaultKeyPair: async function () { + const master_keys = await this._rpc({route: "/vault/rights/get"}); + + // Get the current private key + const private_key = await vault.get_private_key(); + + // Generate new keys + await vault.generate_keys(); + + const public_key = await vault.get_public_key(); + + // Re-encrypt the master keys + const result = {}; + for (const uuid in master_keys) { + result[uuid] = await utils.wrap( + await utils.unwrap(master_keys[uuid], private_key), + public_key + ); + } + + await this._rpc({route: "/vault/rights/store", params: {keys: result}}); + + await this.reload(); + }, + + /** + * Generate a new key pair and re-encrypt the master keys of the vaults + * + * @private + * @param {OdooEvent} ev + */ + _onGenerateKeys: async function (ev) { + ev.stopPropagation(); + var self = this; + + Dialog.confirm( + self, + _t("Do you really want to create a new key pair and set it active?"), + { + confirm_callback: function () { + return self._newVaultKeyPair(); + }, + } + ); + }, + + /** + * Hook into the button to generate new key pairs + * + * @private + */ + renderButtons: function () { + this._super.apply(this, arguments); + + if (this.modelName !== "res.users") return; + + if (this.$buttons) + this.$buttons.on( + "click", + "[name='action_generate_key']", + this._onGenerateKeys.bind(this) + ); + }, + + /** + * Handle changes of vault.right field in the vault view properly by + * sharing the master key with the user + * + * @private + * @param {Object} record + * @param {Object} changes + * @param {Object} options + */ + _changedVaultRightUser: async function (record, changes, options) { + if (!changes.data.user_id) return; + + const params = {user_id: changes.data.user_id.id}; + const user = await this._rpc({route: "/vault/public", params: params}); + + if (!user || !user.public_key) + throw new TypeError("User has no public key"); + + for (const right of record.data.right_ids.data) { + if (right.id === changes.id) { + const key = await vault.share( + record.data.master_key, + user.public_key + ); + await this._applyChanges( + record.id, + { + right_ids: { + operation: "UPDATE", + id: right.id, + data: {key: key}, + }, + }, + options + ); + } + } + }, + + /** + * Handle the deletion of a vault.right field in the vault view properly by + * generating a new master key and re-encrypting everything in the vault to + * deny any future access to the vault. + * + * @private + * @param {Object} record + * @param {Object} changes + * @param {Object} options + */ + _deleteVaultRight: async function (record, changes, options) { + const master_key = await utils.generate_key(); + const current_key = await vault.unwrap(record.data.master_key); + + // Update the rights + for (const right of record.data.right_ids.data) { + if (changes.ids.indexOf(right.id) < 0) { + const key = await vault.wrap_with( + master_key, + right.data.public_key + ); + + await this._applyChanges( + record.id, + { + right_ids: { + operation: "UPDATE", + id: right.id, + data: {key: key}, + }, + }, + options + ); + } + } + + // Re-encrypt the fields + for (const field of record.data.field_ids.data) { + const val = await utils.sym_decrypt( + current_key, + field.data.value, + field.data.iv + ); + const iv = utils.generate_iv_base64(); + const encrypted = await utils.sym_encrypt(master_key, val, iv); + + await this._applyChanges( + record.id, + { + field_ids: { + operation: "UPDATE", + id: field.id, + data: {value: encrypted, iv: iv}, + }, + }, + options + ); + } + + // Re-encrypt the files + for (const file of record.data.file_ids.data) { + const val = await utils.sym_decrypt( + current_key, + file.data.content, + file.data.iv + ); + const iv = utils.generate_iv_base64(); + const encrypted = await utils.sym_encrypt(master_key, val, iv); + + await this._applyChanges( + record.id, + { + file_ids: { + operation: "UPDATE", + id: file.id, + data: {content: encrypted, iv: iv}, + }, + }, + options + ); + } + }, + + /** + * Handle changes to the vault properly and call the specific function for the cases above. + * Generate a master key if there is not one yet + * + * @private + * @param {Object} record + * @param {Object} changes + * @param {Object} options + */ + _applyChangesVault: async function (record, changes, options) { + if (!record.data.master_key && !changes.master_key) { + const master_key = await vault.wrap(await utils.generate_key()); + await this._applyChanges(record.id, {master_key: master_key}, options); + } + + if (changes.right_ids && changes.right_ids.operation === "UPDATE") + await this._changedVaultRightUser(record, changes.right_ids, options); + + if (changes.right_ids && changes.right_ids.operation === "DELETE") { + const self = this; + + Dialog.confirm( + self, + _t( + "This will re-encrypt everything in the vault. Do you want to proceed?" + ), + { + confirm_callback: async function () { + await this._deleteVaultRight( + record, + changes.right_ids, + options + ); + }, + } + ); + } + }, + + /** + * Call the right importer in the import wizard onchange of the content field + * + * @private + * @param {Object} record + * @param {Object} changes + * @param {Object} options + */ + _applyChangesImportWizard: async function (record, changes, options) { + if (!changes.content) return; + + // Try to import the file on the fly and store the compatible JSON in the + // crypted_content field for the python backend + const importer = new Importer(); + const data = await importer.import( + await vault.unwrap(record.data.master_key), + record.data.name, + atob(changes.content) + ); + + if (data) + await this._applyChanges( + record.id, + {crypted_content: JSON.stringify(data)}, + options + ); + }, + + /** + * Check the model of the form and call the above functions for the right case + * + * @private + * @param {String} dataPointID + * @param {Object} changes + * @param {Object} options + */ + _applyChanges: async function (dataPointID, changes, options) { + const result = await this._super.apply(this, arguments); + + const record = this.model.get(dataPointID); + if (record.model === "vault") + await this._applyChangesVault(record, changes, options); + else if (record.model === "vault.send.wizard") + await this._applyChangesSendWizard(record, changes, options); + else if (record.model === "vault.store.wizard") + await this._applyChangesStoreWizard(record, changes, options); + else if (record.model === "vault.import.wizard") + await this._applyChangesImportWizard(record, changes, options); + + return result; + }, + }); +}); diff --git a/vault/static/src/js/vault_export.js b/vault/static/src/js/vault_export.js new file mode 100644 index 0000000000..79f2f3885c --- /dev/null +++ b/vault/static/src/js/vault_export.js @@ -0,0 +1,134 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +odoo.define("vault.export", function (require) { + "use strict"; + + var core = require("web.core"); + var mixins = require("web.mixins"); + var utils = require("vault.utils"); + + var _t = core._t; + + // This class handles the export to different formats by using a standardize + // JSON formatted data generated by the python backend. + // + // JSON format description: + // + // Entries are represented as objects with the following attributes + // `name`, `uuid`, `url`, `note` + // Specific fields of the entry. `uuid` is used for updating existing records + // `childs` + // Child entries + // `fields`, `files` + // List of encypted fields/files with `name`, `iv`, and `value` + // + var VaultExporter = core.Class.extend(mixins.EventDispatcherMixin, { + /** + * Encrypt a field of the above format properly for the backend to store. + * The changes are done inplace. + * + * @private + * @param {CryptoKey} master_key + * @param {Object} node + */ + _export_json_entry: async function (master_key, node) { + const fields = []; + for (const field of node.fields || []) + fields.push({ + name: field.name, + value: await utils.sym_decrypt(master_key, field.value, field.iv), + }); + + const files = []; + for (const file of node.files || []) + files.push({ + name: file.name, + value: await utils.sym_decrypt(master_key, file.value, file.iv), + }); + + const childs = []; + for (const entry of node.childs || []) + childs.push(await this._export_json_entry(master_key, entry)); + + return { + name: node.name || "", + uuid: node.uuid || null, + url: node.url || "", + note: node.note || "", + childs: childs, + fields: fields, + files: files, + }; + }, + + /** + * Decrypt the data fro the JSON export. + * + * @private + * @param {CryptoKey} master_key + * @param {Object} data + * @returns the encrypted entry for the database + */ + _export_json_data: async function (master_key, data) { + const result = []; + for (const node of data) + result.push(await this._export_json_entry(master_key, node)); + return JSON.stringify(result); + }, + + /** + * Export using JSON format. The database is stored in the `data` field of the JSON + * type and is an encrypted JSON object. For the encryption the needed encryption + * parameter `iv`, `salt` and `iterations` are stored in the file. + * This will add `iv` to fields and files and encrypt the `value` + * + * @private + * @param {CryptoKey} master_key + * @param {String} data + * @returns the encrypted entry for the database + */ + _export_json: async function (master_key, data) { + // Get the password for the exported file from the user + const askpass = await utils.askpass( + _t("Please enter the password for the database") + ); + let password = askpass.password || ""; + if (askpass.keyfile) + password += await utils.digest(utils.toBinary(askpass.keyfile)); + + const iv = utils.generate_iv_base64(); + const salt = utils.generate_bytes(utils.SaltLength).buffer; + const iterations = 4000; + const key = await utils.derive_key(password, salt, iterations); + + // Unwrap the master key and decrypt the entries + const content = await this._export_json_data(master_key, JSON.parse(data)); + return { + type: "encrypted", + iv: iv, + salt: utils.toBase64(salt), + data: await utils.sym_encrypt(key, content, iv), + iterations: iterations, + }; + }, + + /** + * The main export functions which checks the file ending and calls the right function + * to handle the rest of the export + * + * @private + * @param {CryptoKey} master_key + * @param {String} filename + * @param {String} content + * @returns the data importable by the backend or false on error + */ + export: async function (master_key, filename, content) { + if (filename.endsWith(".json")) + return await this._export_json(master_key, content); + return false; + }, + }); + + return VaultExporter; +}); diff --git a/vault/static/src/js/vault_import.js b/vault/static/src/js/vault_import.js new file mode 100644 index 0000000000..5cdd8ee302 --- /dev/null +++ b/vault/static/src/js/vault_import.js @@ -0,0 +1,259 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +/* global kdbxweb */ + +odoo.define("vault.import", function (require) { + "use strict"; + + var core = require("web.core"); + var framework = require("web.framework"); + var mixins = require("web.mixins"); + var utils = require("vault.utils"); + + var _t = core._t; + + async function encrypted_field(master_key, name, value) { + if (!value) return null; + + const iv = utils.generate_iv_base64(); + return { + name: name, + iv: iv, + value: await utils.sym_encrypt(master_key, value, iv), + }; + } + + // This class handles the import from different formats by returning + // an importable JSON formatted data which will be handled by the python + // backend. + // + // JSON format description: + // + // Entries are represented as objects with the following attributes + // `name`, `uuid`, `url`, `note` + // Specific fields of the entry. `uuid` is used for updating existing records + // `childs` + // Child entries + // `fields`, `files` + // List of encypted fields/files with `name`, `iv`, and `value` + // + var VaultImporter = core.Class.extend(mixins.EventDispatcherMixin, { + /** + * Encrypt a field of the above format properly for the backend to store. + * The changes are done inplace. + * + * @private + * @param {CryptoKey} master_key + * @param {Object} node + */ + _import_json_entry: async function (master_key, node) { + for (const field of node.fields || []) { + field.iv = utils.generate_iv_base64(); + field.value = await utils.sym_encrypt( + master_key, + field.value, + field.iv + ); + } + + for (const file of node.files || []) { + file.iv = utils.generate_iv_base64(); + file.value = await utils.sym_encrypt(master_key, file.value, file.iv); + } + + for (const entry of node.childs || []) + await this._import_json_entry(master_key, entry); + }, + + /** + * Encrypt the data from the JSON import. This will add `iv` to fields and files + * and encrypt the `value` + * + * @private + * @param {CryptoKey} master_key + * @param {String} data + * @returns the encrypted entry for the database + */ + _import_json_data: async function (master_key, data) { + for (const node of data) await this._import_json_entry(master_key, node); + return data; + }, + + /** + * Load from an encrypted JSON file. Encrypt the data with similar format as + * described above. This will add `iv` to fields and files and encrypt the `value` + * + * @private + * @param {CryptoKey} master_key + * @param {Object} content + * @returns the encrypted entry for the database + */ + _import_encrypted_json: async function (master_key, content) { + const askpass = await utils.askpass( + _t("Please enter the password for the database") + ); + let password = askpass.password || ""; + if (askpass.keyfile) + password += await utils.digest(utils.toBinary(askpass.keyfile)); + + const key = await utils.derive_key( + password, + utils.fromBase64(content.salt), + content.iterations + ); + const result = await utils.sym_decrypt(key, content.data, content.iv); + return await this._import_json_data(master_key, JSON.parse(result)); + }, + + /** + * Import using JSON format. The database is stored in the `data` field of the JSON + * type and is either a JSON object or an encrypted JSON object. For the encryption + * the needed encryption parameter `iv`, `salt` and `iterations` are stored in the + * file. This will add `iv` to fields and files and encrypt the `value` + * + * @private + * @param {CryptoKey} master_key + * @param {String} data + * @returns the encrypted entry for the database + */ + _import_json: async function (master_key, data) { + // Unwrap the master key and encrypt the entries + const result = JSON.parse(data); + switch (result.type) { + case "encrypted": + return await this._import_encrypted_json(master_key, result); + case "raw": + return await this._import_json_data(master_key, result.data); + } + + throw Error(_t("Unsupported file to import")); + }, + + /** + * Encrypt an entry from the kdbx file properly for the backend to store + * + * @private + * @param {CryptoKey} master_key + * @param {Object} entry + * @returns the encrypted entry for the database + */ + _import_kdbx_entry: async function (master_key, entry) { + let pass = entry.fields.Password; + if (pass) pass = pass.getText(); + + const res = { + uuid: entry.uuid && entry.uuid.id, + note: entry.fields.Notes, + name: entry.fields.Title, + url: entry.fields.URL, + fields: [ + await encrypted_field( + master_key, + "Username", + entry.fields.UserName + ), + await encrypted_field(master_key, "Password", pass), + ], + files: [], + }; + + for (const name in entry.binaries) + res.files.push( + await encrypted_field( + master_key, + name, + utils.toBase64(entry.binaries[name].value) + ) + ); + + return res; + }, + + /** + * Handle a kdbx group entry by creating an sub-entry and calling the right functions + * on the childs + * + * @private + * @param {CryptoKey} master_key + * @param {Object} group + * @returns the encrypted entry for the database + */ + _import_kdbx_group: async function (master_key, group) { + const res = { + uuid: group.uuid && group.uuid.id, + name: group.name, + note: group.notes, + childs: [], + }; + + for (const sub_group of group.groups || []) + res.childs.push(await this._import_kdbx_group(master_key, sub_group)); + + for (const entry of group.entries || []) + res.childs.push(await this._import_kdbx_entry(master_key, entry)); + + return res; + }, + + /** + * Load a kdbx file, encrypt the data, and return in the described JSON format + * + * @private + * @param {CryptoKey} master_key + * @param {String} data + * @returns the encrypted data for the backend + */ + _import_kdbx: async function (master_key, data) { + // Get the credentials of the keepass database + const askpass = await utils.askpass( + _t("Please enter the password for the keepass database") + ); + + // TODO: challenge-response + const credentials = new kdbxweb.Credentials( + (askpass.password && + kdbxweb.ProtectedValue.fromString(askpass.password)) || + null, + askpass.keyfile || null + ); + + // Convert the data to an ArrayBuffer + const buffer = utils.fromBinary(data); + + // Decrypt the database + const db = await kdbxweb.Kdbx.load(buffer, credentials); + + try { + // Unwrap the master key, format, and encrypt the database + framework.blockUI(); + const result = []; + for (const group of db.groups) + result.push(await this._import_kdbx_group(master_key, group)); + return result; + } finally { + framework.unblockUI(); + } + }, + + /** + * The main import functions which checks the file ending and calls the right function + * to handle the rest of the import + * + * @private + * @param {CryptoKey} master_key + * @param {String} filename + * @param {String} content + * @returns the data importable by the backend or false on error + */ + import: async function (master_key, filename, content) { + if (filename.endsWith(".json")) + return await this._import_json(master_key, content); + else if (filename.endsWith(".kdbx")) + return await this._import_kdbx(master_key, content); + return false; + }, + }); + + return VaultImporter; +}); diff --git a/vault/static/src/js/vault_inbox.js b/vault/static/src/js/vault_inbox.js new file mode 100644 index 0000000000..37ed894f96 --- /dev/null +++ b/vault/static/src/js/vault_inbox.js @@ -0,0 +1,78 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +odoo.define("vault.inbox", function (require) { + "use strict"; + + require("web.dom_ready"); + var utils = require("vault.utils"); + + var data = {}; + var key = false; + var iv = false; + + const fields = [ + "key", + "iv", + "public", + "encrypted", + "secret", + "encrypted_file", + "filename", + "secret_file", + ]; + + function toggle_required(element, value) { + if (value) element.setAttribute("required", "required"); + else element.removeAttribute("required"); + } + + // Encrypt the value and store it in the right input field + async function encrypt_and_store(value, target) { + // Find all the possible elements which are needed + for (const id of fields) if (!data[id]) data[id] = document.getElementById(id); + + // We expect a public key here otherwise we can't procceed + if (!data.public.value) return; + + const public_key = await utils.load_public_key(data.public.value); + + // Create a new key if not already present + if (!key) { + key = await utils.generate_key(); + data.key.value = await utils.wrap(key, public_key); + } + + // Create a new IV if not already present + if (!iv) { + iv = utils.generate_iv_base64(); + data.iv.value = iv; + } + + // Encrypt the value symmetrically and store it in the field + const val = await utils.sym_encrypt(key, value, iv); + data[target].value = val; + return Boolean(val); + } + + document.getElementById("secret").onchange = async function () { + if (!this.value) return; + + const required = await encrypt_and_store(this.value, "encrypted"); + toggle_required(data.secret, required); + toggle_required(data.secret_file, !required); + }; + + document.getElementById("secret_file").onchange = async function () { + if (!this.files.length) return; + + const file = this.files[0]; + const value = await file.text(); + if (!value) return; + + const required = await encrypt_and_store(value, "encrypted_file"); + toggle_required(data.secret, !required); + toggle_required(data.secret_file, required); + data.filename.value = file.name; + }; +}); diff --git a/vault/static/src/js/vault_utils.js b/vault/static/src/js/vault_utils.js new file mode 100644 index 0000000000..7f69772c79 --- /dev/null +++ b/vault/static/src/js/vault_utils.js @@ -0,0 +1,577 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +/* global Uint8Array */ + +odoo.define("vault.utils", function (require) { + "use strict"; + + var core = require("web.core"); + var Dialog = require("web.Dialog"); + + var _t = core._t; + var qweb = core.qweb; + + const CryptoAPI = window.crypto.subtle; + + // Some basic constants used for the entire vaults + const AsymmetricName = "RSA-OAEP"; + const Hash = "SHA-512"; + const SymmetricName = "AES-GCM"; + + const HashLength = 10; + const IVLength = 12; + const SaltLength = 32; + + const Asymmetric = { + name: AsymmetricName, + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: Hash, + }; + const Symmetric = { + name: SymmetricName, + length: 256, + }; + + /** + * Converts an ArrayBuffer to an ASCII string + * + * @param {ArrayBuffer} buffer + * @returns the data converted to a string + */ + function toBinary(buffer) { + if (!buffer) return ""; + + const chars = Array.from(new Uint8Array(buffer)).map(function (b) { + return String.fromCharCode(b); + }); + return chars.join(""); + } + + /** + * Converts an ASCII string to an ArrayBuffer + * + * @param {String} binary + * @returns the data converted to an ArrayBuffer + */ + function fromBinary(binary) { + const len = binary.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i); + return bytes.buffer; + } + + /** + * Converts an ArrayBuffer to a Base64 encoded string + * + * @param {ArrayBuffer} buffer + * @returns Base64 string + */ + function toBase64(buffer) { + if (!buffer) return ""; + + const chars = Array.from(new Uint8Array(buffer)).map(function (b) { + return String.fromCharCode(b); + }); + return btoa(chars.join("")); + } + + /** + * Converts an Base64 encoded string to an ArrayBuffer + * + * @param {String} base64 + * @returns the data converted to an ArrayBuffer + */ + function fromBase64(base64) { + if (!base64) { + const bytes = new Uint8Array(0); + return bytes.buffer; + } + + const binary_string = atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i); + return bytes.buffer; + } + + /** + * Generate random bytes used for salts or IVs + * + * @param {int} length + * @returns an array with length random bytes + */ + function generate_bytes(length) { + const buf = new Uint8Array(length); + window.crypto.getRandomValues(buf); + return buf; + } + + /** + * Generate random bytes used for salts or IVs encoded as base64 + * + * @returns base64 string + */ + function generate_iv_base64() { + return toBase64(generate_bytes(IVLength)); + } + + /** + * Generate a random secret with specific characters + * + * @param {int} length + * @param {String} characters + * @returns base64 string + */ + function generate_secret(length, characters) { + let result = ""; + const len = characters.length; + for (const k of generate_bytes(length)) + result += characters[Math.floor((len * k) / 256)]; + return result; + } + + /** + * Generate a symmetric key for encrypting and decrypting + * + * @returns symmetric key + */ + async function generate_key() { + return await CryptoAPI.generateKey(Symmetric, true, ["encrypt", "decrypt"]); + } + + /** + * Generate an asymmetric key pair for encrypting, decrypting, wrapping and unwrapping + * + * @returns asymmetric key + */ + async function generate_key_pair() { + return await CryptoAPI.generateKey(Asymmetric, true, [ + "wrapKey", + "unwrapKey", + "decrypt", + "encrypt", + ]); + } + + /** + * Generate a hash of the given data + * + * @param {String} data + * @returns base64 encoded hash of the data + */ + async function digest(data) { + const encoder = new TextEncoder(); + return toBase64(await CryptoAPI.digest(Hash, encoder.encode(data))); + } + + /** + * Ask the user to enter a password using a dialog + * + * @param {String} title of the dialog + * @param {Object} options + * @returns promise + */ + function askpass(title, options = {}) { + var self = this; + + if (options.password === undefined) options.password = true; + if (options.keyfile === undefined) options.keyfile = true; + + return new Promise((resolve, reject) => { + var dialog = new Dialog(self, { + title: title, + $content: $(qweb.render("vault.askpass", options)), + buttons: [ + { + text: _t("Enter"), + classes: "btn-primary", + click: async function () { + const password = this.$("#password").val(); + const keyfile = this.$("#keyfile")[0].files[0]; + + if (!password && !keyfile) { + Dialog.alert(this, _t("Missing password")); + return; + } + + if (options.confirm) { + const confirm = this.$("#confirm").val(); + + if (confirm !== password) { + Dialog.alert( + this, + _t("The passwords aren't matching") + ); + return; + } + } + + dialog.close(); + + let keyfile_content = null; + if (keyfile) + keyfile_content = fromBinary(await keyfile.text()); + + resolve({ + password: password, + keyfile: keyfile_content, + }); + }, + }, + { + text: _t("Cancel"), + click: function () { + dialog.close(); + reject(_t("Cancelled")); + }, + }, + ], + }); + + dialog.open(); + }); + } + + /** + * Ask the user to enter a password using a dialog + * + * @param {String} title of the dialog + * @param {Object} options + * @returns promise + */ + function generate_pass(title, options = {}) { + var self = this; + + const $content = $(qweb.render("vault.generate_pass", options)); + const $password = $content.find("#password")[0]; + const $length = $content.find("#length")[0]; + const $big = $content.find("#big_letter")[0]; + const $small = $content.find("#small_letter")[0]; + const $digits = $content.find("#digits")[0]; + const $special = $content.find("#special")[0]; + var password = null; + + function gen_pass() { + let characters = ""; + if ($big.checked) characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if ($small.checked) characters += "abcdefghijklmnopqrstuvwxyz"; + if ($digits.checked) characters += "0123456789"; + if ($special.checked) characters += "!?$%&/()[]{}|<>,;.:-_#+*\\"; + + if (characters) + $password.innerHTML = password = generate_secret( + $length.value, + characters + ); + } + + $length.onchange = $big.onchange = $small.onchange = $digits.onchange = $special.onchange = gen_pass; + + gen_pass(); + + return new Promise((resolve, reject) => { + var dialog = new Dialog(self, { + title: title, + $content: $content, + buttons: [ + { + text: _t("Enter"), + classes: "btn-primary", + click: async function () { + if (!password) throw new Error(_t("Missing password")); + + dialog.close(); + resolve(password); + }, + }, + { + text: _t("Cancel"), + click: function () { + dialog.close(); + reject(_t("Cancelled")); + }, + }, + ], + }); + + dialog.open(); + }); + } + + /** + * Derive a key using the given data, salt and iterations using PBKDF2 + * + * @param {String} data + * @param {String} salt + * @param {int} iterations + * @returns the derived key + */ + async function derive_key(data, salt, iterations) { + const enc = new TextEncoder(); + const material = await CryptoAPI.importKey( + "raw", + enc.encode(data), + "PBKDF2", + false, + ["deriveBits", "deriveKey"] + ); + + return await CryptoAPI.deriveKey( + { + name: "PBKDF2", + salt: salt, + iterations: iterations, + hash: Hash, + }, + material, + Symmetric, + false, + ["wrapKey", "unwrapKey", "encrypt", "decrypt"] + ); + } + + /** + * Encrypt the data using a public key + * + * @param {CryptoKey} public_key + * @param {String} data + * @returns the encrypted data + */ + async function asym_encrypt(public_key, data) { + if (!data) return data; + + const enc = new TextEncoder(); + return toBase64( + await CryptoAPI.encrypt( + {name: AsymmetricName}, + public_key, + enc.encode(data) + ) + ); + } + + /** + * Decrypt the data using the own private key + * + * @param {CryptoKey} private_key + * @param {String} crypted + * @returns the decrypted data + */ + async function asym_decrypt(private_key, crypted) { + if (!crypted) return crypted; + + const dec = new TextDecoder(); + return dec.decode( + await CryptoAPI.decrypt( + {name: AsymmetricName}, + private_key, + fromBase64(crypted) + ) + ); + } + + /** + * Symmetrically encrypt the data using a master key + * + * @param {CryptoKey} key + * @param {String} data + * @param {String} iv + * @returns the encrypted data + */ + async function sym_encrypt(key, data, iv) { + if (!data) return data; + + const hash = await digest(data); + const enc = new TextEncoder(); + return toBase64( + await CryptoAPI.encrypt( + {name: SymmetricName, iv: fromBase64(iv), tagLength: 128}, + key, + enc.encode(hash.slice(0, HashLength) + data) + ) + ); + } + + /** + * Symmetrically decrypt the data using a master key + * + * @param {CryptoKey} key + * @param {String} crypted + * @param {String} iv + * @returns the decrypted data + */ + async function sym_decrypt(key, crypted, iv) { + if (!crypted) return crypted; + + try { + const dec = new TextDecoder(); + const message = dec.decode( + await CryptoAPI.decrypt( + {name: SymmetricName, iv: fromBase64(iv), tagLength: 128}, + key, + fromBase64(crypted) + ) + ); + + const data = message.slice(HashLength); + const hash = await digest(data); + // Compare the hash and return if integer + if (hash.slice(0, HashLength) === message.slice(0, HashLength)) return data; + + console.error("Invalid data hash"); + // Wrong hash + return null; + } catch (err) { + console.error(err); + return null; + } + } + + /** + * Load a public key + * + * @param {String} public_key + * @returns the public key as CryptoKey + */ + async function load_public_key(public_key) { + return await CryptoAPI.importKey( + "spki", + fromBase64(public_key), + Asymmetric, + true, + ["wrapKey", "encrypt"] + ); + } + + /** + * Load a private key + * + * @param {String} private_key + * @param {CryptoKey} key + * @param {String} iv + * @returns the private key as CryptoKey + */ + async function load_private_key(private_key, key, iv) { + return await CryptoAPI.unwrapKey( + "pkcs8", + fromBase64(private_key), + key, + {name: SymmetricName, iv: fromBase64(iv), tagLength: 128}, + Asymmetric, + true, + ["unwrapKey", "decrypt"] + ); + } + + /** + * Export a public key in spki format + * + * @param {CryptoKey} public_key + * @returns the public key as string + */ + async function export_public_key(public_key) { + return toBase64(await CryptoAPI.exportKey("spki", public_key)); + } + + /** + * Export a private key in pkcs8 format + * + * @param {String} private_key + * @param {CryptoKey} key + * @param {String} iv + * @returns the public key as CryptoKey + */ + async function export_private_key(private_key, key, iv) { + return toBase64( + await CryptoAPI.wrapKey("pkcs8", private_key, key, { + name: SymmetricName, + iv: iv, + tagLength: 128, + }) + ); + } + + /** + * Wrap the master key with the own public key + * + * @param {CryptoKey} key + * @param {CryptoKey} public_key + * @returns wrapped master key + */ + async function wrap(key, public_key) { + return toBase64(await CryptoAPI.wrapKey("raw", key, public_key, Asymmetric)); + } + + /** + * Unwrap the master key with the own private key + * + * @param {CryptoKey} key + * @param {CryptoKey} private_key + * @returns unwrapped master key + */ + async function unwrap(key, private_key) { + return await CryptoAPI.unwrapKey( + "raw", + fromBase64(key), + private_key, + Asymmetric, + Symmetric, + true, + ["encrypt", "decrypt"] + ); + } + + /** + * Capitalize each word of the string + * + * @param {String} s + * @returns capitalized string + */ + function capitalize(s) { + return s.toLowerCase().replace(/\b\w/g, function (c) { + return c.toUpperCase(); + }); + } + + return { + // Constants + Asymmetric: Asymmetric, + AsymmetricName: AsymmetricName, + Hash: Hash, + HashLength: HashLength, + IVLength: IVLength, + SaltLength: SaltLength, + Symmetric: Symmetric, + SymmetricName: SymmetricName, + + // Crypto utility functions + askpass: askpass, + asym_decrypt: asym_decrypt, + asym_encrypt: asym_encrypt, + derive_key: derive_key, + digest: digest, + export_private_key: export_private_key, + export_public_key: export_public_key, + generate_bytes: generate_bytes, + generate_iv_base64: generate_iv_base64, + generate_key: generate_key, + generate_key_pair: generate_key_pair, + generate_pass: generate_pass, + generate_secret: generate_secret, + load_private_key: load_private_key, + load_public_key: load_public_key, + sym_decrypt: sym_decrypt, + sym_encrypt: sym_encrypt, + unwrap: unwrap, + wrap: wrap, + + // Utility functions + capitalize: capitalize, + fromBase64: fromBase64, + fromBinary: fromBinary, + toBase64: toBase64, + toBinary: toBinary, + }; +}); diff --git a/vault/static/src/js/vault_widget.js b/vault/static/src/js/vault_widget.js new file mode 100644 index 0000000000..5ef07b0429 --- /dev/null +++ b/vault/static/src/js/vault_widget.js @@ -0,0 +1,544 @@ +// © 2021 Florian Kantelberg - initOS GmbH +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +/* global ArrayBuffer, Uint8Array */ + +odoo.define("vault.fields", function (require) { + "use strict"; + + var core = require("web.core"); + var basic_fields = require("web.basic_fields"); + var download = require("web.download"); + var Exporter = require("vault.export"); + var registry = require("web.field_registry"); + var utils = require("vault.utils"); + var vault = require("vault"); + + var _t = core._t; + var QWeb = core.qweb; + + var VaultAbstract = { + /** + * Set the value by encrypting it + * + * @param {String} value + * @param {Object} options + * @returns promise for the encryption + */ + _setValue: function (value, options) { + const self = this; + const _super = this._super; + return this._encrypt(value).then(function (data) { + _super.call(self, data, options); + }); + }, + + /** + * Set the value of a different field + * + * @param {String} field + * @param {String} value + */ + _setFieldValue: function (field, value) { + const data = {}; + data[field] = value; + + this.trigger_up("field_changed", { + dataPointID: this.dataPointID, + changes: data, + }); + }, + + /** + * Extract the IV or generate a new one if needed + * + * @returns the IV to use + */ + _getIV: function () { + // IV already read. Reuse it + if (this.iv) return this.iv; + + // Read the IV from the field + this.iv = this.recordData[this.field_iv]; + if (this.iv) return this.iv; + + // Generate a new IV + this.iv = utils.generate_iv_base64(); + this._setFieldValue(this.field_iv, this.iv); + return this.iv; + }, + + /** + * Extract the master key of the vault or generate a new one + * + * @returns the master key to use + */ + _getMasterKey: async function () { + // Check if the master key is already extracted + if (this.key) return await vault.unwrap(this.key); + + // Get the wrapped master key from the field + this.key = this.recordData[this.field_key]; + if (this.key) return await vault.unwrap(this.key); + + // Generate a new master key and write it to the field + const key = await utils.generate_key(); + this.key = await vault.wrap(key); + this._setFieldValue(this.field_key, this.key); + return key; + }, + + /** + * Toggle if the value is shown or hidden, and decrypt the value if shown + * + * @private + * @param {OdooEvent} ev + */ + _onShowValue: async function (ev) { + ev.stopPropagation(); + + this.decrypted = !this.decrypted; + if (this.decrypted) this.decrypted_value = await this._decrypt(this.value); + else this.decrypted_value = false; + + this._render(); + }, + + /** + * Copy the decrypted value to the clipboard + * + * @private + * @param {OdooEvent} ev + */ + _onCopyValue: async function (ev) { + ev.stopPropagation(); + + const value = await this._decrypt(this.value); + await navigator.clipboard.writeText(value); + }, + + /** + * Send the value with an internal user + * + * @private + * @param {OdooEvent} ev + */ + _onSendValue: async function (ev) { + ev.stopPropagation(); + + const key = await utils.generate_key(); + const iv = utils.generate_iv_base64(); + const value = await this._decrypt(this.value); + + this.do_action({ + type: "ir.actions.act_window", + title: _t("Send the secret to another user"), + target: "new", + res_model: "vault.send.wizard", + views: [[false, "form"]], + context: { + default_secret: await utils.sym_encrypt(key, value, iv), + default_iv: iv, + default_key: await vault.wrap(key), + }, + }); + }, + + /** + * Save the content in an entry of a vault + * + * @private + * @param {OdooEvent} ev + */ + _onSaveValue: async function (ev) { + ev.stopPropagation(); + + const key = await utils.generate_key(); + const iv = utils.generate_iv_base64(); + const value = await this._decrypt(this.value); + const store_model = this.store_model || "vault.field"; + + this.do_action({ + type: "ir.actions.act_window", + title: _t("Store the secret in a vault"), + target: "new", + res_model: "vault.store.wizard", + views: [[false, "form"]], + context: { + default_model: store_model, + default_secret_temporary: await utils.sym_encrypt(key, value, iv), + default_iv: iv, + default_key: await vault.wrap(key), + }, + }); + }, + + /** + * Decrypt data with the master key stored in the vault + * + * @param {String} data + * @returns the decrypted data + */ + _decrypt: async function (data) { + const iv = this._getIV(); + const key = await this._getMasterKey(); + return await utils.sym_decrypt(key, data, iv); + }, + + /** + * Encrypt data with the master key stored in the vault + * + * @param {String} data + * @returns the encrypted data + */ + _encrypt: async function (data) { + const iv = this._getIV(); + const key = await this._getMasterKey(); + return await utils.sym_encrypt(key, data, iv); + }, + }; + + // Basic field widget of the vault + var VaultField = basic_fields.InputField.extend(VaultAbstract, { + supportedFieldTypes: ["char"], + tagName: "div", + events: _.extend({}, basic_fields.InputField.prototype.events, { + "click .o_vault_show": "_onShowValue", + "click .o_vault_clipboard": "_onCopyValue", + "click .o_vault_generate": "_onGenerateValue", + "click .o_vault_send": "_onSendValue", + }), + className: "o_vault o_field_char", + template: "FieldVault", + + /** + * 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 || "master_key"; + this.field_iv = this.attrs.iv || "iv"; + this.decrypted = false; + }, + + /** + * Generate a secret + * + * @private + * @param {OdooEvent} ev + */ + _onGenerateValue: async function (ev) { + ev.stopPropagation(); + + const password = await utils.generate_pass(); + this.$el.find("input.o_vault_value")[0].value = password; + this._setValue(password, {}); + }, + + /** + * Render the decrypted value or the stars + * + * @private + */ + _renderReadonly: function () { + this._renderValue(this.decrypted_value || "********"); + }, + + /** + * Adapt the maxlength + * + * @private + * @override + */ + _renderEdit: function () { + if (this.field.size && this.field.size > 0) + this.$el.attr("maxlength", this.field.size); + return this._super.apply(this, arguments); + }, + + /** + * Render the decrypted value or the stars + * + * @private + * @param {String} value to render + */ + _renderValue: function (value) { + this.$el.html( + QWeb.render(this.template, { + widget: self, + value: value, + show: !this.decrypted, + }) + ); + }, + + /** + * Decrypt the value and show in edit mode + * + * @private + * @param {JQuery} $input + * @returns the element + */ + _prepareInput: function ($input) { + const self = this; + const inputAttrs = { + placeholder: self.attrs.placeholder || "", + type: "text", + }; + this.$input = $input || $(""); + this.$input.addClass("o_input"); + this.$input.attr(inputAttrs); + + this._decrypt(this.value).then(function (data) { + self.$input.val(self._formatValue(data)); + }); + + return this.$input; + }, + + /** + * @override + * @returns {String} the content of the input + */ + _getValue: function () { + return this.$("input").val(); + }, + }); + + // Widget used for using encrypted files + var VaultFile = basic_fields.FieldBinaryFile.extend(VaultAbstract, { + className: "o_vault", + + /** + * 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 || "master_key"; + this.field_iv = this.attrs.iv || "iv"; + }, + + /** + * Handle on save correctly by decrypting the value before and starting the download + * + * @private + * @param {OdooEvent} ev + */ + on_save_as: async function (ev) { + if (!this.value) { + this.do_warn( + _t("Save As..."), + _t("The field is empty, there's nothing to save!") + ); + ev.stopPropagation(); + } else if (this.res_id) { + ev.stopPropagation(); + + const filename_fieldname = this.attrs.filename; + const base64 = atob(await this._decrypt(this.value)); + const buffer = new ArrayBuffer(base64.length); + const arr = new Uint8Array(buffer); + for (let i = 0; i < base64.length; i++) arr[i] = base64.charCodeAt(i); + + const blob = new Blob([arr]); + download(blob, this.recordData[filename_fieldname] || ""); + } + }, + }); + + // Widget used for using export + var VaultExportFile = basic_fields.FieldBinaryFile.extend(VaultAbstract, { + className: "o_vault", + events: _.extend({}, basic_fields.AbstractFieldBinary.prototype.events, { + click: function (event) { + if (this.mode === "readonly" && this.value) this.on_save_as(event); + }, + "click .o_input": function () { + this.$(".o_input_file").click(); + }, + }), + + /** + * 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 || "master_key"; + }, + + /** + * Render the widget always like the normal widget in readonly mode + * + * @private + * @override + */ + _render: function () { + if (this.value) { + this.$el.empty().append($("").addClass("fa fa-download")); + this.$el.css("cursor", "pointer"); + + if (this.filename_value) this.$el.append(" " + this.filename_value); + } else this.$el.css("cursor", "not-allowed"); + }, + + /** + * Handle on save correctly by decrypting the value before and starting the download + * + * @private + * @param {OdooEvent} ev + */ + on_save_as: async function (ev) { + if (this.value) { + ev.stopPropagation(); + + const exporter = new Exporter(); + const content = JSON.stringify( + await exporter.export( + await this._getMasterKey(), + this.filename_value, + this.value + ) + ); + const buffer = new ArrayBuffer(content.length); + const arr = new Uint8Array(buffer); + for (let i = 0; i < content.length; i++) arr[i] = content.charCodeAt(i); + + const blob = new Blob([arr]); + download(blob, this.filename_value || ""); + } else { + this.do_warn( + _t("Save As..."), + _t("The field is empty, there's nothing to save!") + ); + ev.stopPropagation(); + } + }, + }); + + var VaultInboxField = VaultField.extend(VaultAbstract, { + store_model: "vault.field", + events: _.extend({}, VaultField.prototype.events, { + "click .o_vault_show": "_onShowValue", + "click .o_vault_clipboard": "_onCopyValue", + "click .o_vault_save": "_onSaveValue", + }), + template: "FieldVaultInbox", + + /** + * Prepare the widget by evaluating the field attributes and setting the defaults + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + + this.field_iv = this.attrs.iv || "iv"; + this.field_key = this.attrs.key || "key"; + this.decrypted = false; + }, + + /** + * Decrypt the data with the private key of the vault + * + * @private + * @param {String} data + * @returns the decrypted data + */ + _decrypt: async function (data) { + const iv = this.recordData[this.field_iv]; + const wrapped_key = this.recordData[this.field_key]; + + if (!iv || !wrapped_key) return false; + + const key = await vault.unwrap(wrapped_key); + return await utils.sym_decrypt(key, data, iv); + }, + + /** + * Render the decrypted value or the stars + * + * @private + */ + _renderEdit: function () { + this._renderReadonly(); + }, + }); + + // Widget used to view shared incoming secrets encrypted with public keys + var VaultInboxFile = VaultFile.extend(VaultAbstract, { + store_model: "vault.file", + template: "FileVaultInbox", + events: _.extend({}, 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.decrypted = false; + }, + + _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 data with the private key of the vault + * + * @private + * @param {String} data + * @returns the decrypted data + */ + _decrypt: async function (data) { + const iv = this.recordData[this.field_iv]; + const wrapped_key = this.recordData[this.field_key]; + + if (!iv || !wrapped_key) return false; + + const key = await vault.unwrap(wrapped_key); + return btoa(await utils.sym_decrypt(key, data, iv)); + }, + }); + + registry.add("vault", VaultField); + registry.add("vault_file", VaultFile); + registry.add("vault_export", VaultExportFile); + registry.add("vault_inbox", VaultInboxField); + registry.add("vault_inbox_file", VaultInboxFile); + + return { + VaultAbstract: VaultAbstract, + VaultField: VaultField, + VaultFile: VaultFile, + VaultExportFile: VaultExportFile, + VaultInboxFile: VaultInboxFile, + VaultInboxField: VaultInboxField, + }; +}); diff --git a/vault/static/src/scss/vault.scss b/vault/static/src/scss/vault.scss new file mode 100644 index 0000000000..16838f4283 --- /dev/null +++ b/vault/static/src/scss/vault.scss @@ -0,0 +1,7 @@ +.o_vault button { + padding: 0; +} + +.o_vault_inbox { + white-space: pre-wrap; +} diff --git a/vault/static/src/xml/templates.xml b/vault/static/src/xml/templates.xml new file mode 100644 index 0000000000..f7cbeb3e7f --- /dev/null +++ b/vault/static/src/xml/templates.xml @@ -0,0 +1,178 @@ + + + + + Key Management + + + + +