From 11a2115cb4a5e99459d1c1d160cc17f3a9342812 Mon Sep 17 00:00:00 2001
From: fkantelberg
Date: Thu, 18 Jan 2024 19:13:38 +0100
Subject: [PATCH] [MIG][16.0] vault: Migration and restructuring for 16.0
---
setup/vault/odoo/addons/vault | 1 +
setup/vault/setup.py | 6 +
setup/vault_share/odoo/addons/vault_share | 1 +
setup/vault_share/setup.py | 6 +
vault/README.rst | 12 +-
vault/__manifest__.py | 10 +-
vault/controllers/main.py | 4 +
vault/models/abstract_vault_field.py | 26 +-
vault/models/vault.py | 2 +-
vault/models/vault_entry.py | 27 +-
vault/models/vault_log.py | 6 +-
vault/models/vault_right.py | 35 +-
vault/static/description/index.html | 8 +-
vault/static/src/backend/controller.esm.js | 382 ++++++++++++
vault/static/src/backend/export.esm.js | 8 +-
vault/static/src/backend/fields/templates.xml | 121 ++++
.../backend/fields/vault_export_file.esm.js | 45 ++
.../src/backend/fields/vault_field.esm.js | 200 +++++++
.../src/backend/fields/vault_file.esm.js | 61 ++
.../backend/fields/vault_inbox_field.esm.js | 49 ++
.../backend/fields/vault_inbox_file.esm.js | 49 ++
.../backend/fields/vault_inbox_mixin.esm.js | 64 ++
.../src/backend/fields/vault_mixin.esm.js | 197 ++++++
vault/static/src/backend/import.esm.js | 2 +-
vault/static/src/backend/templates.xml | 88 ---
vault/static/src/backend/vault.esm.js | 16 +-
vault/static/src/backend/vault.scss | 12 +-
vault/static/src/common/utils.esm.js | 31 +-
vault/static/src/frontend/inbox.esm.js | 23 +-
vault/static/src/legacy/vault_controller.js | 389 ------------
vault/static/src/legacy/vault_widget.js | 563 ------------------
vault/tests/test_controller.py | 45 +-
vault/tests/test_widgets.py | 6 +-
vault/views/res_users_views.xml | 2 +-
vault/views/vault_entry_views.xml | 2 +-
vault/views/vault_inbox_views.xml | 6 +-
vault/wizards/vault_export_wizard.py | 34 +-
vault/wizards/vault_export_wizard.xml | 2 +-
vault/wizards/vault_send_wizard.py | 6 +-
vault_share/README.rst | 17 +-
vault_share/__manifest__.py | 10 +-
vault_share/controllers/main.py | 1 +
.../migrations/16.0.1.1.1/post-migrate.py | 13 +
vault_share/models/vault_share.py | 12 +-
vault_share/readme/ROADMAP.rst | 1 +
vault_share/static/description/index.html | 14 +-
.../static/src/backend/fields/templates.xml | 46 ++
.../src/backend/fields/vault_field.esm.js | 45 ++
.../src/backend/fields/vault_pin_field.esm.js | 110 ++++
.../backend/fields/vault_share_field.esm.js | 15 +
.../backend/fields/vault_share_file.esm.js | 15 +
.../backend/fields/vault_share_mixin.esm.js | 164 +++++
vault_share/static/src/backend/templates.xml | 57 --
vault_share/static/src/common/utils.esm.js | 6 +-
vault_share/static/src/frontend/share.esm.js | 48 +-
vault_share/static/src/legacy/vault_fields.js | 55 --
.../static/src/legacy/vault_share_widget.js | 367 ------------
vault_share/tests/test_share.py | 25 +-
vault_share/views/templates.xml | 19 +-
vault_share/views/vault_share_views.xml | 5 +-
60 files changed, 1874 insertions(+), 1718 deletions(-)
create mode 120000 setup/vault/odoo/addons/vault
create mode 100644 setup/vault/setup.py
create mode 120000 setup/vault_share/odoo/addons/vault_share
create mode 100644 setup/vault_share/setup.py
create mode 100644 vault/static/src/backend/controller.esm.js
create mode 100644 vault/static/src/backend/fields/templates.xml
create mode 100644 vault/static/src/backend/fields/vault_export_file.esm.js
create mode 100644 vault/static/src/backend/fields/vault_field.esm.js
create mode 100644 vault/static/src/backend/fields/vault_file.esm.js
create mode 100644 vault/static/src/backend/fields/vault_inbox_field.esm.js
create mode 100644 vault/static/src/backend/fields/vault_inbox_file.esm.js
create mode 100644 vault/static/src/backend/fields/vault_inbox_mixin.esm.js
create mode 100644 vault/static/src/backend/fields/vault_mixin.esm.js
delete mode 100644 vault/static/src/legacy/vault_controller.js
delete mode 100644 vault/static/src/legacy/vault_widget.js
create mode 100644 vault_share/migrations/16.0.1.1.1/post-migrate.py
create mode 100644 vault_share/readme/ROADMAP.rst
create mode 100644 vault_share/static/src/backend/fields/templates.xml
create mode 100644 vault_share/static/src/backend/fields/vault_field.esm.js
create mode 100644 vault_share/static/src/backend/fields/vault_pin_field.esm.js
create mode 100644 vault_share/static/src/backend/fields/vault_share_field.esm.js
create mode 100644 vault_share/static/src/backend/fields/vault_share_file.esm.js
create mode 100644 vault_share/static/src/backend/fields/vault_share_mixin.esm.js
delete mode 100644 vault_share/static/src/backend/templates.xml
delete mode 100644 vault_share/static/src/legacy/vault_fields.js
delete mode 100644 vault_share/static/src/legacy/vault_share_widget.js
diff --git a/setup/vault/odoo/addons/vault b/setup/vault/odoo/addons/vault
new file mode 120000
index 0000000000..4ead31e201
--- /dev/null
+++ b/setup/vault/odoo/addons/vault
@@ -0,0 +1 @@
+../../../../vault
\ No newline at end of file
diff --git a/setup/vault/setup.py b/setup/vault/setup.py
new file mode 100644
index 0000000000..28c57bb640
--- /dev/null
+++ b/setup/vault/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/vault_share/odoo/addons/vault_share b/setup/vault_share/odoo/addons/vault_share
new file mode 120000
index 0000000000..d8d730a844
--- /dev/null
+++ b/setup/vault_share/odoo/addons/vault_share
@@ -0,0 +1 @@
+../../../../vault_share
\ No newline at end of file
diff --git a/setup/vault_share/setup.py b/setup/vault_share/setup.py
new file mode 100644
index 0000000000..28c57bb640
--- /dev/null
+++ b/setup/vault_share/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/vault/README.rst b/vault/README.rst
index 1848e73c9c..50b4c38e64 100644
--- a/vault/README.rst
+++ b/vault/README.rst
@@ -7,7 +7,7 @@ Vault
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:e0b69ed2cd488c2635fec51457c0cb50c1bbd628cc768e68cd8d1d80c944ce2e
+ !! source digest: sha256:f5ab04a25cc568dea3db60cecb5e549d44da1d67f710b01698316ebfaaa79d28
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@@ -17,13 +17,13 @@ Vault
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
- :target: https://github.com/OCA/server-auth/tree/15.0/vault
+ :target: https://github.com/OCA/server-auth/tree/16.0/vault
:alt: OCA/server-auth
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/server-auth-15-0/server-auth-15-0-vault
+ :target: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-vault
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
- :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=15.0
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -65,7 +65,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues `_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-`feedback `_.
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -95,6 +95,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
-This module is part of the `OCA/server-auth `_ project on GitHub.
+This module is part of the `OCA/server-auth `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/vault/__manifest__.py b/vault/__manifest__.py
index 09eb85ea28..2585046f3a 100644
--- a/vault/__manifest__.py
+++ b/vault/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Vault",
"summary": "Password vault integration in Odoo",
"license": "AGPL-3",
- "version": "15.0.2.1.0",
+ "version": "16.0.2.1.0",
"website": "https://github.com/OCA/server-auth",
"application": True,
"author": "initOS GmbH, Odoo Community Association (OCA)",
@@ -38,14 +38,10 @@
],
"web.assets_backend": [
"vault/static/lib/**/*.min.js",
+ "vault/static/src/**/*.xml",
"vault/static/src/common/*.js",
"vault/static/src/backend/*.scss",
- "vault/static/src/backend/*.js",
- "vault/static/src/legacy/vault_controller.js",
- "vault/static/src/legacy/vault_widget.js",
- ],
- "web.assets_qweb": [
- "vault/static/src/**/*.xml",
+ "vault/static/src/backend/**/*.js",
],
"web.tests_assets": [
"vault/static/tests/**/*.js",
diff --git a/vault/controllers/main.py b/vault/controllers/main.py
index c7ec7e70a8..1ca361dda3 100644
--- a/vault/controllers/main.py
+++ b/vault/controllers/main.py
@@ -4,6 +4,7 @@
import logging
from odoo import _, http
+from odoo.exceptions import AccessDenied
from odoo.http import request
_logger = logging.getLogger(__name__)
@@ -136,6 +137,9 @@ def vault_replace(self, data):
vault = request.env["vault"].with_context(vault_skip_log=True)
for changes in data:
record = vault.env[changes["model"]].browse(changes["id"])
+ if not record.vault_id.allowed_write:
+ raise AccessDenied()
+
vault |= record.vault_id
if record._name in ("vault.field", "vault.file"):
record.write({k: v for k, v in changes.items() if k in ["iv", "value"]})
diff --git a/vault/models/abstract_vault_field.py b/vault/models/abstract_vault_field.py
index 65c42ca55b..691d7b7d03 100644
--- a/vault/models/abstract_vault_field.py
+++ b/vault/models/abstract_vault_field.py
@@ -33,29 +33,25 @@ def _compute_master_key(self):
rec.master_key = rec.vault_id.master_key
def log_change(self, action):
- self.ensure_one()
if self.env.context.get("vault_skip_log"):
return
- 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)
+ for rec in self:
+ rec.entry_id.log_info(
+ f"{action} value {rec.name} of {rec.entry_id.complete_name} "
+ f"by {self.env.user.display_name}"
+ )
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
res.log_change("Created")
return res
def unlink(self):
- for rec in self:
- rec.log_change("Deleted")
-
+ self.log_change("Deleted")
return super().unlink()
def write(self, values):
- for rec in self:
- rec.log_change("Changed")
-
+ self.log_change("Changed")
return super().write(values)
diff --git a/vault/models/vault.py b/vault/models/vault.py
index 8d6064c51e..5aef99f230 100644
--- a/vault/models/vault.py
+++ b/vault/models/vault.py
@@ -1,4 +1,4 @@
-# © 2021 Florian Kantelberg - initOS GmbH
+# © 2021-2024 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
diff --git a/vault/models/vault_entry.py b/vault/models/vault_entry.py
index 03677f0359..1f553c3561 100644
--- a/vault/models/vault_entry.py
+++ b/vault/models/vault_entry.py
@@ -140,28 +140,27 @@ def _search_expired(self, operator, value):
return ["|", ("expire_date", ">=", datetime.now()), ("expire_date", "=", False)]
def log_change(self, action):
- self.ensure_one()
if self.env.context.get("vault_skip_log"):
return
- self.log_info(
- _("%(action)s entry %(name)s by %(user)s")
- % {
- "action": action,
- "name": self.complete_name,
- "user": self.env.user.display_name,
- }
- )
+ for rec in self:
+ rec.log_info(
+ _("%(action)s entry %(name)s by %(user)s")
+ % {
+ "action": action,
+ "name": rec.complete_name,
+ "user": rec.env.user.display_name,
+ }
+ )
- @api.model_create_single
- def create(self, values):
- res = super().create(values)
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
res.log_change("Created")
return res
def unlink(self):
- for rec in self:
- rec.log_change("Deleted")
+ self.log_change("Deleted")
return super().unlink()
diff --git a/vault/models/vault_log.py b/vault/models/vault_log.py
index 9189dd122d..3f44569c53 100644
--- a/vault/models/vault_log.py
+++ b/vault/models/vault_log.py
@@ -38,9 +38,9 @@ def _get_log_state(self):
("error", _("Error")),
]
- @api.model
- def create(self, values):
- res = super().create(values)
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
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
index 287a69961b..1f9fc3bc77 100644
--- a/vault/models/vault_right.py
+++ b/vault/models/vault_right.py
@@ -69,25 +69,25 @@ def _compute_public_key(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 ["create", "write", "share", "delete"]
- if getattr(self, f"perm_{right}", False)
- ]
+ for rec in self:
+ rights = ", ".join(
+ sorted(
+ ["read"]
+ + [
+ right
+ for right in ["create", "write", "share", "delete"]
+ if getattr(rec, f"perm_{right}", False)
+ ]
+ )
)
- )
- self.vault_id.log_info(
- f"Grant access to user {self.user_id.display_name}: {rights}"
- )
+ rec.vault_id.log_info(
+ f"Grant access to user {rec.user_id.display_name}: {rights}"
+ )
- @api.model
- def create(self, values):
- res = super().create(values)
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
if not res.allowed_share and not res.env.su:
self.raise_access_error()
@@ -98,8 +98,7 @@ def write(self, values):
res = super().write(values)
perms = ["perm_write", "perm_delete", "perm_share", "perm_create"]
if any(x in values for x in perms):
- for rec in self:
- rec.log_access()
+ self.log_access()
return res
diff --git a/vault/static/description/index.html b/vault/static/description/index.html
index 4cc4272298..475b75d97a 100644
--- a/vault/static/description/index.html
+++ b/vault/static/description/index.html
@@ -367,9 +367,9 @@ Vault
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:e0b69ed2cd488c2635fec51457c0cb50c1bbd628cc768e68cd8d1d80c944ce2e
+!! source digest: sha256:f5ab04a25cc568dea3db60cecb5e549d44da1d67f710b01698316ebfaaa79d28
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-
+
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.
This modules requires a secure context for the browser to work properly.
@@ -413,7 +413,7 @@
Bugs are tracked on GitHub Issues .
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-feedback .
+feedback .
Do not contact contributors directly about support or help with technical issues.
@@ -437,7 +437,7 @@
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
-
This module is part of the OCA/server-auth project on GitHub.
+
This module is part of the OCA/server-auth project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute .
diff --git a/vault/static/src/backend/controller.esm.js b/vault/static/src/backend/controller.esm.js
new file mode 100644
index 0000000000..3310fd0d2b
--- /dev/null
+++ b/vault/static/src/backend/controller.esm.js
@@ -0,0 +1,382 @@
+/** @odoo-module alias=vault.controller **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import Dialog from "web.Dialog";
+import {FormController} from "@web/views/form/form_controller";
+import Importer from "vault.import";
+import {_lt} from "@web/core/l10n/translation";
+import framework from "web.framework";
+import {patch} from "@web/core/utils/patch";
+import {useService} from "@web/core/utils/hooks";
+import utils from "vault.utils";
+import vault from "vault";
+
+patch(FormController.prototype, "vault", {
+ /**
+ * Re-encrypt the key if the user is getting selected
+ *
+ * @private
+ */
+ async _vaultSendWizard() {
+ const record = this.model.root;
+ if (record.resModel !== "vault.send.wizard") return;
+
+ if (!record.data.user_id || !record.data.public) return;
+
+ const key = await vault.unwrap(record.data.key);
+ await record.update({key_user: await vault.wrap_with(key, record.data.public)});
+ },
+
+ /**
+ * Re-encrypt the key if the entry is getting selected
+ *
+ * @private
+ * @param {Object} record
+ * @param {Object} changes
+ * @param {Object} options
+ */
+ async _vaultStoreWizard() {
+ const record = this.model.root;
+ if (
+ !record.data.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 record.update({
+ secret: await utils.sym_encrypt(master_key, secret, record.data.iv),
+ });
+ },
+
+ /**
+ * Generate a new key pair for the current user
+ *
+ * @private
+ */
+ async _newVaultKeyPair() {
+ // 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 master_keys = await this.rpc("/vault/rights/get");
+ let 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("/vault/rights/store", {keys: result});
+
+ // Re-encrypt the inboxes to not loose it
+ const inbox_keys = await this.rpc("/vault/inbox/get");
+ result = {};
+ for (const uuid in inbox_keys) {
+ result[uuid] = await utils.wrap(
+ await utils.unwrap(inbox_keys[uuid], private_key),
+ public_key
+ );
+ }
+
+ await this.rpc("/vault/inbox/store", {keys: result});
+ },
+
+ /**
+ * Generate a new key pair and re-encrypt the master keys of the vaults
+ *
+ * @private
+ */
+ async _vaultRegenerateKey() {
+ if (!utils.supported()) return;
+
+ var self = this;
+
+ Dialog.confirm(
+ self,
+ _lt("Do you really want to create a new key pair and set it active?"),
+ {
+ confirm_callback: function () {
+ return self._newVaultKeyPair();
+ },
+ }
+ );
+ },
+
+ /**
+ * 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 {Boolean} verify
+ * @param {Boolean} force
+ */
+ async _reencryptVault(verify = false, force = false) {
+ const record = this.model.root;
+
+ await vault._ensure_keys();
+
+ const self = this;
+ const master_key = await utils.generate_key();
+ const current_key = await vault.unwrap(record.data.master_key);
+
+ // This stores the additional changes made to rights, fields, and files
+ const changes = [];
+ const problems = [];
+
+ async function reencrypt(model, type) {
+ // Load the entire data from the database
+ const records = await self.model.orm.searchRead(
+ model,
+ [["vault_id", "=", record.resId]],
+ ["iv", "value", "name", "entry_name"],
+ {
+ context: {vault_reencrypt: true},
+ limit: 0,
+ }
+ );
+
+ for (const rec of records) {
+ const val = await utils.sym_decrypt(current_key, rec.value, rec.iv);
+ if (val === null) {
+ problems.push(
+ _.str.sprintf(
+ _lt("%s '%s' of entry '%s'"),
+ type,
+ rec.name,
+ rec.entry_name
+ )
+ );
+ continue;
+ }
+
+ const iv = utils.generate_iv_base64();
+ const encrypted = await utils.sym_encrypt(master_key, val, iv);
+
+ changes.push({
+ id: rec.id,
+ model: model,
+ value: encrypted,
+ iv: iv,
+ });
+ }
+ }
+
+ framework.blockUI();
+ try {
+ // Update the rights. Load without limit
+ const rights = await self.model.orm.searchRead(
+ "vault.right",
+ [["vault_id", "=", record.resId]],
+ ["public_key"],
+ {limit: 0}
+ );
+
+ for (const right of rights) {
+ const key = await vault.wrap_with(master_key, right.public_key);
+
+ changes.push({
+ id: right.id,
+ model: "vault.right",
+ key: key,
+ });
+ }
+
+ // Re-encrypt vault.field and vault.file
+ await reencrypt("vault.field", "Field");
+ await reencrypt("vault.file", "File");
+
+ if (problems.length && !force) {
+ framework.unblockUI();
+
+ Dialog.alert(self, "", {
+ title: _lt("The following entries are broken:"),
+ $content: $("
").html(problems.join(" \n")),
+ });
+ }
+
+ if (!verify) {
+ await this.rpc("/vault/replace", {data: changes});
+ await this.model.root.load();
+ }
+ } finally {
+ framework.unblockUI();
+ }
+ },
+
+ /**
+ * Call the right importer in the import wizard onchange of the content field
+ *
+ * @private
+ */
+ async _vaultImportWizard() {
+ const record = this.model.root;
+ if (record.resModel !== "vault.import.wizard") 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(record.data.content)
+ );
+
+ if (data) await record.update({crypted_content: JSON.stringify(data)});
+ },
+
+ /**
+ * Ensure that a vault.right as the shared master_key set
+ *
+ * @private
+ * @param {Object} root
+ * @param {Object} right
+ */
+ async _vaultEnsureRightKey(root, right) {
+ if (!root.data.master_key || right.data.master_key) return;
+
+ const params = {user_id: right.data.user_id[0]};
+ const user = await this.rpc("/vault/public", params);
+
+ if (!user || !user.public_key) throw new TypeError("User has no public key");
+
+ await right.update({
+ key: await vault.share(root.data.master_key, user.public_key),
+ });
+ },
+
+ /**
+ * Ensures that the master_key of the vault and right lines are set
+ *
+ * @private
+ */
+ async _vaultEnsureKeys() {
+ const root = this.model.root;
+ if (root.resModel !== "vault") return;
+
+ if (!root.data.master_key)
+ await root.update({
+ master_key: await vault.wrap(await utils.generate_key()),
+ });
+
+ if (root.data.right_ids)
+ for (const right of root.data.right_ids.records)
+ await this._vaultEnsureRightKey(root, right);
+ },
+
+ /**
+ * Check the model of the form and call the above functions for the right case
+ *
+ * @private
+ * @param {Object} button
+ */
+ async _vaultAction(button) {
+ if (!utils.supported()) return false;
+
+ const root = this.model.root;
+ switch (root.resModel) {
+ case "res.users":
+ if (button && button.name === "vault_generate_key") {
+ await this._vaultRegenerateKey();
+ return false;
+ }
+ break;
+ case "vault":
+ if (button && button.name === "vault_reencrypt") {
+ await this._reencryptVault(false, true);
+ return false;
+ } else if (button && button.name === "vault_verify") {
+ await this._reencryptVault(true, false);
+ return false;
+ }
+
+ await this._vaultEnsureKeys();
+ break;
+
+ case "vault.send.wizard":
+ await this._vaultSendWizard();
+ break;
+
+ case "vault.store.wizard":
+ await this._vaultStoreWizard();
+ break;
+
+ case "vault.import.wizard":
+ await this._vaultImportWizard();
+ break;
+ }
+
+ return true;
+ },
+
+ /**
+ * Add the required rpc service to the controller which will be used to
+ * get/store information from/to the vault controller
+ */
+ setup() {
+ this._super(...arguments);
+ this.rpc = useService("rpc");
+ },
+
+ /**
+ * Hook into the relevant functions
+ */
+ async create() {
+ const _super = this._super.bind(this);
+ if (this.model.root.isDirty) await this._vaultAction();
+
+ const ret = await _super(...arguments);
+ return ret;
+ },
+
+ async onPagerUpdate() {
+ const _super = this._super.bind(this);
+ if (this.model.root.isDirty) await this._vaultAction();
+ return await _super(...arguments);
+ },
+
+ async saveButtonClicked() {
+ const _super = this._super.bind(this);
+ if (this.model.root.isDirty) await this._vaultAction();
+ return await _super(...arguments);
+ },
+
+ async beforeLeave() {
+ const _super = this._super.bind(this);
+ if (this.model.root.isDirty) await this._vaultAction();
+ return await _super(...arguments);
+ },
+
+ async beforeUnload() {
+ const _super = this._super.bind(this);
+ if (this.model.root.isDirty) await this._vaultAction();
+ return await _super(...arguments);
+ },
+
+ async beforeExecuteActionButton(clickParams) {
+ const _super = this._super.bind(this);
+ if (clickParams.special !== "cancel") {
+ const _continue = await this._vaultAction(clickParams);
+ if (!_continue) return false;
+ }
+
+ return await _super(...arguments);
+ },
+});
diff --git a/vault/static/src/backend/export.esm.js b/vault/static/src/backend/export.esm.js
index abb324f313..f14ac14cec 100644
--- a/vault/static/src/backend/export.esm.js
+++ b/vault/static/src/backend/export.esm.js
@@ -1,8 +1,8 @@
/** @odoo-module alias=vault.export **/
-// © 2021-2022 Florian Kantelberg - initOS GmbH
+// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-import {_t} from "web.core";
+import {_lt} from "@web/core/l10n/translation";
import utils from "vault.utils";
// This class handles the export to different formats by using a standardize
@@ -86,7 +86,7 @@ export default class VaultExporter {
async _export_json(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")
+ _lt("Please enter the password for the database")
);
let password = askpass.password || "";
if (askpass.keyfile)
@@ -94,7 +94,7 @@ export default class VaultExporter {
const iv = utils.generate_iv_base64();
const salt = utils.generate_bytes(utils.SaltLength).buffer;
- const iterations = 4000;
+ const iterations = utils.Derive.iterations;
const key = await utils.derive_key(password, salt, iterations);
// Unwrap the master key and decrypt the entries
diff --git a/vault/static/src/backend/fields/templates.xml b/vault/static/src/backend/fields/templates.xml
new file mode 100644
index 0000000000..b5619af48b
--- /dev/null
+++ b/vault/static/src/backend/fields/templates.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ *******
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ *******
+
+
+
+
+
+
+
+
+
+
+
+
+
+ *******
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vault/static/src/backend/fields/vault_export_file.esm.js b/vault/static/src/backend/fields/vault_export_file.esm.js
new file mode 100644
index 0000000000..779a31d612
--- /dev/null
+++ b/vault/static/src/backend/fields/vault_export_file.esm.js
@@ -0,0 +1,45 @@
+/** @odoo-module alias=vault.export.file **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import {BinaryField} from "@web/views/fields/binary/binary_field";
+import Exporter from "vault.export";
+import VaultMixin from "vault.mixin";
+import {_lt} from "@web/core/l10n/translation";
+import {downloadFile} from "@web/core/network/download";
+import {registry} from "@web/core/registry";
+import utils from "vault.utils";
+
+export default class VaultExportFile extends VaultMixin(BinaryField) {
+ /**
+ * Call the exporter and download the finalized file
+ */
+ async onFileDownload() {
+ if (!this.props.value) {
+ this.do_warn(
+ _lt("Save As..."),
+ _lt("The field is empty, there's nothing to save!")
+ );
+ } else if (utils.supported()) {
+ const exporter = new Exporter();
+ const content = JSON.stringify(
+ await exporter.export(
+ await this._getMasterKey(),
+ this.state.fileName,
+ this.props.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]);
+ await downloadFile(blob, this.state.fileName || "");
+ }
+ }
+}
+
+VaultExportFile.template = "vault.FileVaultExport";
+
+registry.category("fields").add("vault_export_file", VaultExportFile);
diff --git a/vault/static/src/backend/fields/vault_field.esm.js b/vault/static/src/backend/fields/vault_field.esm.js
new file mode 100644
index 0000000000..e21979ccfb
--- /dev/null
+++ b/vault/static/src/backend/fields/vault_field.esm.js
@@ -0,0 +1,200 @@
+/** @odoo-module alias=vault.field **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import {Component, useEffect, useRef, useState} from "@odoo/owl";
+import {useBus, useService} from "@web/core/utils/hooks";
+import VaultMixin from "vault.mixin";
+import {_lt} from "@web/core/l10n/translation";
+import {getActiveHotkey} from "@web/core/hotkeys/hotkey_service";
+import {registry} from "@web/core/registry";
+import utils from "vault.utils";
+
+export default class VaultField extends VaultMixin(Component) {
+ setup() {
+ super.setup();
+
+ this.action = useService("action");
+ this.input = useRef("input");
+ this.span = useRef("span");
+ this.state = useState({
+ decrypted: false,
+ decryptedValue: "",
+ isDirty: false,
+ lastSetValue: null,
+ });
+
+ const self = this;
+ useEffect(
+ (inputEl) => {
+ if (inputEl) {
+ const onInput = self.onInput.bind(self);
+ const onKeydown = self.onKeydown.bind(self);
+
+ inputEl.addEventListener("input", onInput);
+ inputEl.addEventListener("keydown", onKeydown);
+ return () => {
+ inputEl.removeEventListener("input", onInput);
+ inputEl.removeEventListener("keydown", onKeydown);
+ };
+ }
+ },
+ () => [self.input.el]
+ );
+
+ useEffect(() => {
+ const isInvalid = self.props.record
+ ? self.props.record.isInvalid(self.props.name)
+ : false;
+
+ if (self.input.el && !self.state.isDirty && !isInvalid) {
+ Promise.resolve(self.getValue()).then((val) => {
+ if (!self.input.el) return;
+
+ if (val) self.input.el.value = val;
+ else if (val !== "")
+ self.props.record.setInvalidField(self.props.name);
+ });
+ self.state.lastSetValue = self.input.el.value;
+ }
+ });
+
+ useBus(self.env.bus, "RELATIONAL_MODEL:WILL_SAVE_URGENTLY", () =>
+ self.commitChanges(true)
+ );
+ useBus(self.env.bus, "RELATIONAL_MODEL:NEED_LOCAL_CHANGES", (ev) =>
+ ev.detail.proms.push(self.commitChanges())
+ );
+ }
+
+ /**
+ * Open a dialog to generate a new secret
+ *
+ * @param {Object} ev
+ */
+ async _onGenerateValue(ev) {
+ ev.stopPropagation();
+
+ const password = await utils.generate_pass();
+ await this.storeValue(password);
+ }
+
+ /**
+ * Toggle between visible and invisible secret
+ *
+ * @param {Object} ev
+ */
+ async _onShowValue(ev) {
+ ev.stopPropagation();
+
+ this.state.decrypted = !this.state.decrypted;
+ if (this.state.decrypted) {
+ this.state.decryptedValue = await this._decrypt(this.props.value);
+ } else {
+ this.state.decryptedValue = "";
+ }
+
+ await this.showValue();
+ }
+
+ /**
+ * Copy the decrypted secret to the clipboard
+ *
+ * @param {Object} ev
+ */
+ async _onCopyValue(ev) {
+ ev.stopPropagation();
+
+ const value = await this._decrypt(this.props.value);
+ await navigator.clipboard.writeText(value);
+ }
+
+ /**
+ * Send the secret to an inbox of an user
+ *
+ * @param {Object} ev
+ */
+ async _onSendValue(ev) {
+ ev.stopPropagation();
+
+ await this.sendValue(this.props.value, "");
+ }
+
+ /**
+ * Get the decrypted value or a placeholder
+ *
+ * @returns the decrypted value or a placeholder
+ */
+ get formattedValue() {
+ if (!this.props.value) return "";
+ if (this.state.decrypted) return this.state.decryptedValue || "*******";
+ return "*******";
+ }
+
+ /**
+ * Decrypt the value of the field
+ *
+ * @returns decrypted value
+ */
+ async getValue() {
+ return await this._decrypt(this.props.value);
+ }
+
+ /**
+ * Update the value shown
+ */
+ async showValue() {
+ this.span.el.innerHTML = this.formattedValue;
+ }
+
+ /**
+ * Handle input event and set the state to dirty
+ *
+ * @param {Object} ev
+ */
+ onInput(ev) {
+ ev.stopPropagation();
+
+ this.state.isDirty = ev.target.value !== this.lastSetValue;
+ if (this.props.setDirty) this.props.setDirty(this.state.isDirty);
+ }
+
+ /**
+ * Commit the changes of the input field to the record
+ *
+ * @param {Boolean} urgent
+ */
+ async commitChanges(urgent) {
+ if (!this.input.el) return;
+
+ this.state.isDirty = this.input.el.value !== this.lastSetValue;
+ if (this.state.isDirty || urgent) {
+ this.state.isDirty = false;
+
+ const val = this.input.el.value || false;
+ if (val !== (this.props.value || false)) {
+ this.state.lastSetValue = this.input.el.value;
+ await this.storeValue(val);
+ this.props.setDirty(this.state.isDirty);
+ }
+ }
+ }
+
+ /**
+ * Handle keyboard events and trigger changes
+ *
+ * @param {Object} ev
+ */
+ onKeydown(ev) {
+ ev.stopPropagation();
+
+ const hotkey = getActiveHotkey(ev);
+ if (["enter", "tab", "shift+tab"].includes(hotkey)) this.commitChanges(false);
+ }
+}
+
+VaultField.displayName = _lt("Vault Field");
+VaultField.supportedTypes = ["char"];
+VaultField.template = "vault.FieldVault";
+
+registry.category("fields").add("vault_field", VaultField);
diff --git a/vault/static/src/backend/fields/vault_file.esm.js b/vault/static/src/backend/fields/vault_file.esm.js
new file mode 100644
index 0000000000..e2aba7eff0
--- /dev/null
+++ b/vault/static/src/backend/fields/vault_file.esm.js
@@ -0,0 +1,61 @@
+/** @odoo-module alias=vault.file **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import {BinaryField} from "@web/views/fields/binary/binary_field";
+import VaultMixin from "vault.mixin";
+import {_lt} from "@web/core/l10n/translation";
+import {downloadFile} from "@web/core/network/download";
+import {registry} from "@web/core/registry";
+import {useService} from "@web/core/utils/hooks";
+import utils from "vault.utils";
+
+export default class VaultFile extends VaultMixin(BinaryField) {
+ setup() {
+ super.setup();
+
+ this.action = useService("action");
+ }
+
+ async update({data, name}) {
+ const encrypted = await this._encrypt(data);
+ return await super.update({data: encrypted, name: name});
+ }
+
+ /**
+ * Send the secret to an inbox of an user
+ *
+ * @param {Object} ev
+ */
+ async _onSendValue(ev) {
+ ev.stopPropagation();
+
+ await this.sendValue("", this.props.value, this.state.fileName);
+ }
+
+ /**
+ * Decrypt the file and download it
+ */
+ async onFileDownload() {
+ if (!this.props.value) {
+ this.do_warn(
+ _lt("Save As..."),
+ _lt("The field is empty, there's nothing to save!")
+ );
+ } else if (utils.supported()) {
+ const decrypted = await this._decrypt(this.props.value);
+ const base64 = atob(decrypted);
+ 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]);
+ await downloadFile(blob, this.state.fileName || "");
+ }
+ }
+}
+
+VaultFile.displayName = _lt("Vault File");
+VaultFile.template = "vault.FileVault";
+
+registry.category("fields").add("vault_file", VaultFile);
diff --git a/vault/static/src/backend/fields/vault_inbox_field.esm.js b/vault/static/src/backend/fields/vault_inbox_field.esm.js
new file mode 100644
index 0000000000..fd9ae7bbfa
--- /dev/null
+++ b/vault/static/src/backend/fields/vault_inbox_field.esm.js
@@ -0,0 +1,49 @@
+/** @odoo-module alias=vault.inbox.field **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import VaultField from "vault.field";
+import VaultInboxMixin from "vault.inbox.mixin";
+import {_lt} from "@web/core/l10n/translation";
+import {registry} from "@web/core/registry";
+import utils from "vault.utils";
+import vault from "vault";
+
+export default class VaultInboxField extends VaultInboxMixin(VaultField) {
+ /**
+ * Save the content in an entry of a vault
+ *
+ * @private
+ */
+ async _onSaveValue() {
+ await this.saveValue("vault.field", this.props.value);
+ }
+
+ /**
+ * Decrypt the data with the private key of the vault
+ *
+ * @private
+ * @param {String} data
+ * @returns the decrypted data
+ */
+ async _decrypt(data) {
+ if (!utils.supported()) return null;
+
+ const iv = this.props.record.data[this.props.fieldIV];
+ const wrapped_key = this.props.record.data[this.props.fieldKey];
+
+ if (!iv || !wrapped_key) return false;
+
+ const key = await vault.unwrap(wrapped_key);
+ return await utils.sym_decrypt(key, data, iv);
+ }
+}
+
+VaultInboxField.defaultProps = {
+ ...VaultField.defaultProps,
+ fieldKey: "key",
+};
+VaultInboxField.displayName = _lt("Vault Inbox Field");
+VaultInboxField.template = "vault.FieldVaultInbox";
+
+registry.category("fields").add("vault_inbox_field", VaultInboxField);
diff --git a/vault/static/src/backend/fields/vault_inbox_file.esm.js b/vault/static/src/backend/fields/vault_inbox_file.esm.js
new file mode 100644
index 0000000000..224af9e122
--- /dev/null
+++ b/vault/static/src/backend/fields/vault_inbox_file.esm.js
@@ -0,0 +1,49 @@
+/** @odoo-module alias=vault.inbox.file **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import VaultFile from "vault.file";
+import VaultInboxMixin from "vault.inbox.mixin";
+import {_lt} from "@web/core/l10n/translation";
+import {registry} from "@web/core/registry";
+import utils from "vault.utils";
+import vault from "vault";
+
+export default class VaultInboxFile extends VaultInboxMixin(VaultFile) {
+ /**
+ * Save the content in an entry of a vault
+ *
+ * @private
+ */
+ async _onSaveValue() {
+ await this.saveValue("vault.file", this.props.value, this.state.fileName);
+ }
+
+ /**
+ * Decrypt the data with the private key of the vault
+ *
+ * @private
+ * @param {String} data
+ * @returns the decrypted data
+ */
+ async _decrypt(data) {
+ if (!utils.supported()) return null;
+
+ const iv = this.props.record.data[this.props.fieldIV];
+ const wrapped_key = this.props.record.data[this.props.fieldKey];
+
+ if (!iv || !wrapped_key) return false;
+
+ const key = await vault.unwrap(wrapped_key);
+ return await utils.sym_decrypt(key, data, iv);
+ }
+}
+
+VaultInboxFile.defaultProps = {
+ ...VaultFile.defaultProps,
+ fieldKey: "key",
+};
+VaultInboxFile.displayName = _lt("Vault Inbox File");
+VaultInboxFile.template = "vault.FileVaultInbox";
+
+registry.category("fields").add("vault_inbox_file", VaultInboxFile);
diff --git a/vault/static/src/backend/fields/vault_inbox_mixin.esm.js b/vault/static/src/backend/fields/vault_inbox_mixin.esm.js
new file mode 100644
index 0000000000..9bc3f2e2aa
--- /dev/null
+++ b/vault/static/src/backend/fields/vault_inbox_mixin.esm.js
@@ -0,0 +1,64 @@
+/** @odoo-module alias=vault.inbox.mixin **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import {_lt} from "@web/core/l10n/translation";
+import {useService} from "@web/core/utils/hooks";
+import utils from "vault.utils";
+import vault from "vault";
+
+export default (x) => {
+ class Extended extends x {
+ setup() {
+ super.setup();
+
+ if (!this.action) this.action = useService("action");
+ }
+
+ /**
+ * Save the content in an entry of a vault
+ *
+ * @private
+ * @param {String} model
+ * @param {String} value
+ * @param {String} name
+ */
+ async saveValue(model, value, name = "") {
+ const key = await utils.generate_key();
+ const iv = utils.generate_iv_base64();
+ const decrypted = await this._decrypt(value);
+
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ title: _lt("Store the secret in a vault"),
+ target: "new",
+ res_model: "vault.store.wizard",
+ views: [[false, "form"]],
+ context: {
+ default_model: model,
+ default_secret_temporary: await utils.sym_encrypt(
+ key,
+ decrypted,
+ iv
+ ),
+ default_name: name,
+ default_iv: iv,
+ default_key: await vault.wrap(key),
+ },
+ });
+ }
+ }
+
+ Extended.props = {
+ ...x.props,
+ storeModel: {type: String, optional: true},
+ };
+
+ Extended.extractProps = ({attrs}) => {
+ return {
+ storeModel: attrs.store,
+ };
+ };
+
+ return Extended;
+};
diff --git a/vault/static/src/backend/fields/vault_mixin.esm.js b/vault/static/src/backend/fields/vault_mixin.esm.js
new file mode 100644
index 0000000000..96b447639b
--- /dev/null
+++ b/vault/static/src/backend/fields/vault_mixin.esm.js
@@ -0,0 +1,197 @@
+/** @odoo-module alias=vault.mixin **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
+// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import {_lt} from "@web/core/l10n/translation";
+import {standardFieldProps} from "@web/views/fields/standard_field_props";
+import utils from "vault.utils";
+import vault from "vault";
+
+export default (x) => {
+ class Extended extends x {
+ supported() {
+ return utils.supported();
+ }
+
+ // Control the visibility of the buttons
+ get showButton() {
+ return this.props.value;
+ }
+ get copyButton() {
+ return this.props.value;
+ }
+ get sendButton() {
+ return this.props.value;
+ }
+ get saveButton() {
+ return this.props.value;
+ }
+ get generateButton() {
+ return true;
+ }
+ get isNew() {
+ return Boolean(this.model.record.isNew);
+ }
+
+ /**
+ * Set the value by encrypting it
+ *
+ * @param {String} value
+ * @param {Object} options
+ */
+ async storeValue(value, options) {
+ if (!utils.supported()) return;
+
+ const encrypted = await this._encrypt(value);
+ await this.props.update(encrypted, options);
+ }
+
+ /**
+ * Send the value to an inbox
+ *
+ * @param {String} value_field
+ * @param {String} value_file
+ * @param {String} filename
+ */
+ async sendValue(value_field = "", value_file = "", filename = "") {
+ if (!utils.supported()) return;
+
+ if (!value_field && !value_file) return;
+
+ let enc_field = false,
+ enc_file = false;
+
+ // Prepare the key and iv for the reencryption
+ const key = await utils.generate_key();
+ const iv = utils.generate_iv_base64();
+
+ // Reencrypt the field
+ if (value_field) {
+ const decrypted = await this._decrypt(value_field);
+ enc_field = await utils.sym_encrypt(key, decrypted, iv);
+ }
+
+ // Reencrypt the file
+ if (value_file) {
+ const decrypted = await this._decrypt(value_file);
+ enc_file = await utils.sym_encrypt(key, decrypted, iv);
+ }
+
+ // Call the wizard to handle the user selection and storage
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ title: _lt("Send the secret to another user"),
+ target: "new",
+ res_model: "vault.send.wizard",
+ views: [[false, "form"]],
+ context: {
+ default_secret: enc_field,
+ default_secret_file: enc_file,
+ default_filename: filename || false,
+ default_iv: iv,
+ default_key: await vault.wrap(key),
+ },
+ });
+ }
+
+ /**
+ * Set the value of a different field
+ *
+ * @param {String} field
+ * @param {String} value
+ */
+ async _setFieldValue(field, value) {
+ this.props.record.update({[field]: value});
+ }
+
+ /**
+ * Extract the IV or generate a new one if needed
+ *
+ * @returns the IV to use
+ */
+ async _getIV() {
+ if (!utils.supported()) return null;
+
+ // Read the IV from the field
+ let iv = this.props.record.data[this.props.fieldIV];
+ if (iv) return iv;
+
+ // Generate a new IV
+ iv = utils.generate_iv_base64();
+ await this._setFieldValue(this.props.fieldIV, iv);
+ return iv;
+ }
+
+ /**
+ * Extract the master key of the vault or generate a new one
+ *
+ * @returns the master key to use
+ */
+ async _getMasterKey() {
+ if (!utils.supported()) return null;
+
+ // 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.props.record.data[this.props.fieldKey];
+ 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);
+ await this._setFieldValue(this.props.fieldKey, this.key);
+ return key;
+ }
+
+ /**
+ * Decrypt data with the master key stored in the vault
+ *
+ * @param {String} data
+ * @returns the decrypted data
+ */
+ async _decrypt(data) {
+ if (!utils.supported()) return null;
+
+ const iv = await 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
+ */
+ async _encrypt(data) {
+ if (!utils.supported()) return null;
+
+ const iv = await this._getIV();
+ const key = await this._getMasterKey();
+ return await utils.sym_encrypt(key, data, iv);
+ }
+ }
+
+ Extended.defaultProps = {
+ ...x.defaultProps,
+ fieldIV: "iv",
+ fieldKey: "master_key",
+ };
+ Extended.props = {
+ ...standardFieldProps,
+ ...x.props,
+ fieldKey: {type: String, optional: true},
+ fieldIV: {type: String, optional: true},
+ };
+ Extended.extractProps = ({attrs, field}) => {
+ const extract_props = x.extractProps || (() => ({}));
+ return {
+ ...extract_props({attrs, field}),
+ fieldKey: attrs.key,
+ fieldIV: attrs.iv,
+ };
+ };
+
+ return Extended;
+};
diff --git a/vault/static/src/backend/import.esm.js b/vault/static/src/backend/import.esm.js
index c2033d740b..3fccc76846 100644
--- a/vault/static/src/backend/import.esm.js
+++ b/vault/static/src/backend/import.esm.js
@@ -1,5 +1,5 @@
/** @odoo-module alias=vault.import **/
-// © 2021-2022 Florian Kantelberg - initOS GmbH
+// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
/* global kdbxweb */
diff --git a/vault/static/src/backend/templates.xml b/vault/static/src/backend/templates.xml
index b6e8cb73b1..e4512a105d 100644
--- a/vault/static/src/backend/templates.xml
+++ b/vault/static/src/backend/templates.xml
@@ -1,41 +1,5 @@
-
-
- Key Management
-
-
-
-
-
-
-
-
-
-
Please enter your password or upload a keyfile:
@@ -133,56 +97,4 @@
-
-
-
- *******
-
-
-
-
-
-
-
-
-
-
-
-
-
- *******
-
-
-
-
-
-
-
-
-
-
-
- *******
-
-
-
-
-
-
-
-
-
-
diff --git a/vault/static/src/backend/vault.esm.js b/vault/static/src/backend/vault.esm.js
index 96af61106e..cf5cfd29d0 100644
--- a/vault/static/src/backend/vault.esm.js
+++ b/vault/static/src/backend/vault.esm.js
@@ -1,12 +1,11 @@
/** @odoo-module alias=vault **/
-// © 2021-2022 Florian Kantelberg - initOS GmbH
+// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-// web.dom_ready
-import * as utils from "vault.utils";
import {_t} from "web.core";
import ajax from "web.ajax";
import {session} from "@web/session";
+import utils from "vault.utils";
// Database name on the browser
const Database = "vault";
@@ -48,7 +47,7 @@ class Vault {
* @override
*/
constructor() {
- var self = this;
+ const self = this;
function waitAndCheck() {
if (!utils.supported()) return null;
@@ -93,6 +92,8 @@ class Vault {
*/
async _check_key_migration(password = null) {
if (!this.version) await this._export_to_database(password);
+ if (this.iterations < utils.Derive.iterations)
+ await this._export_to_database(password);
}
/**
@@ -131,9 +132,12 @@ class Vault {
* @private
*/
async _ensure_keys() {
+ // If the object store has the keys it's done
+ if (this.uuid && !this.time) await this._import_from_store();
+
// Check if the keys expired
const now = new Date();
- if (now - this.time <= Expiration) return;
+ if (!this.time || now - this.time <= Expiration) return;
// Keys expired means that we have to get them again
this.keys = this.time = null;
@@ -290,7 +294,7 @@ class Vault {
async _export_to_database(password = null) {
// Generate salt for the user key
this.salt = utils.generate_bytes(utils.SaltLength).buffer;
- this.iterations = 4000;
+ this.iterations = utils.Derive.iterations;
this.version = 1;
// Wrap the private key with the master key of the user
diff --git a/vault/static/src/backend/vault.scss b/vault/static/src/backend/vault.scss
index 16838f4283..4c3ec328a1 100644
--- a/vault/static/src/backend/vault.scss
+++ b/vault/static/src/backend/vault.scss
@@ -1,7 +1,11 @@
-.o_vault button {
- padding: 0;
-}
-
.o_vault_inbox {
white-space: pre-wrap;
}
+
+.o_field_cell .o_vault .o_vault_buttons {
+ float: right;
+}
+
+.o_vault .o_input {
+ width: auto;
+}
diff --git a/vault/static/src/common/utils.esm.js b/vault/static/src/common/utils.esm.js
index 95a5a3114b..5217851541 100644
--- a/vault/static/src/common/utils.esm.js
+++ b/vault/static/src/common/utils.esm.js
@@ -1,5 +1,5 @@
/** @odoo-module alias=vault.utils **/
-// © 2021-2022 Florian Kantelberg - initOS GmbH
+// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import {_t, qweb} from "web.core";
@@ -8,22 +8,24 @@ import Dialog from "web.Dialog";
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,
+ name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: Hash,
};
+const Derive = {
+ name: "PBKDF2",
+ iterations: 600001,
+};
const Symmetric = {
- name: SymmetricName,
+ name: "AES-GCM",
length: 256,
};
@@ -313,14 +315,14 @@ async function derive_key(data, salt, iterations) {
const material = await CryptoAPI.importKey(
"raw",
enc.encode(data),
- "PBKDF2",
+ Derive.name,
false,
["deriveBits", "deriveKey"]
);
return await CryptoAPI.deriveKey(
{
- name: "PBKDF2",
+ name: Derive.name,
salt: salt,
iterations: iterations,
hash: Hash,
@@ -344,7 +346,7 @@ async function asym_encrypt(public_key, data) {
const enc = new TextEncoder();
return toBase64(
- await CryptoAPI.encrypt({name: AsymmetricName}, public_key, enc.encode(data))
+ await CryptoAPI.encrypt({name: Asymmetric.name}, public_key, enc.encode(data))
);
}
@@ -361,7 +363,7 @@ async function asym_decrypt(private_key, crypted) {
const dec = new TextDecoder();
return dec.decode(
await CryptoAPI.decrypt(
- {name: AsymmetricName},
+ {name: Asymmetric.name},
private_key,
fromBase64(crypted)
)
@@ -383,7 +385,7 @@ async function sym_encrypt(key, data, iv) {
const enc = new TextEncoder();
return toBase64(
await CryptoAPI.encrypt(
- {name: SymmetricName, iv: fromBase64(iv), tagLength: 128},
+ {name: Symmetric.name, iv: fromBase64(iv), tagLength: 128},
key,
enc.encode(hash.slice(0, HashLength) + data)
)
@@ -405,7 +407,7 @@ async function sym_decrypt(key, crypted, iv) {
const dec = new TextDecoder();
const message = dec.decode(
await CryptoAPI.decrypt(
- {name: SymmetricName, iv: fromBase64(iv), tagLength: 128},
+ {name: Symmetric.name, iv: fromBase64(iv), tagLength: 128},
key,
fromBase64(crypted)
)
@@ -451,7 +453,7 @@ async function load_private_key(private_key, key, iv) {
"pkcs8",
fromBase64(private_key),
key,
- {name: SymmetricName, iv: fromBase64(iv), tagLength: 128},
+ {name: Symmetric.name, iv: fromBase64(iv), tagLength: 128},
Asymmetric,
true,
["unwrapKey", "decrypt"]
@@ -479,7 +481,7 @@ async function export_public_key(public_key) {
async function export_private_key(private_key, key, iv) {
return toBase64(
await CryptoAPI.wrapKey("pkcs8", private_key, key, {
- name: SymmetricName,
+ name: Symmetric.name,
iv: iv,
tagLength: 128,
})
@@ -531,13 +533,12 @@ function capitalize(s) {
export default {
// Constants
Asymmetric: Asymmetric,
- AsymmetricName: AsymmetricName,
+ Derive: Derive,
Hash: Hash,
HashLength: HashLength,
IVLength: IVLength,
SaltLength: SaltLength,
Symmetric: Symmetric,
- SymmetricName: SymmetricName,
// Crypto utility functions
askpass: askpass,
diff --git a/vault/static/src/frontend/inbox.esm.js b/vault/static/src/frontend/inbox.esm.js
index a7b2363a72..759c032c13 100644
--- a/vault/static/src/frontend/inbox.esm.js
+++ b/vault/static/src/frontend/inbox.esm.js
@@ -1,5 +1,5 @@
-/** @odoo-modules alias=vault.inbox **/
-// © 2021-2022 Florian Kantelberg - initOS GmbH
+/** @odoo-module alias=vault.inbox **/
+// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import utils from "vault.utils";
@@ -72,10 +72,23 @@ 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 reader = new FileReader();
+ let content = null;
- const required = await encrypt_and_store(value, "encrypted_file");
+ const promise = new Promise((resolve) => {
+ reader.onload = () => {
+ if (reader.result.indexOf(",") >= 0) content = reader.result.split(",")[1];
+ resolve();
+ };
+ });
+
+ reader.readAsDataURL(file);
+
+ await promise;
+
+ if (!content) return;
+
+ const required = await encrypt_and_store(content, "encrypted_file");
toggle_required(data.secret, !required);
toggle_required(data.secret_file, required);
data.filename.value = file.name;
diff --git a/vault/static/src/legacy/vault_controller.js b/vault/static/src/legacy/vault_controller.js
deleted file mode 100644
index 93bfc2f094..0000000000
--- a/vault/static/src/legacy/vault_controller.js
+++ /dev/null
@@ -1,389 +0,0 @@
-// © 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 framework = require("web.framework");
- var Importer = require("vault.import");
- var utils = require("vault.utils");
- var vault = require("vault");
-
- var _t = core._t;
-
- FormController.include({
- events: _.extend({}, FormController.prototype.events, {
- "click [name='vault_reencrypt']": "_clickReencryptVault",
- "click [name='vault_verify']": "_clickVerifyVault",
- }),
-
- /**
- * 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 () {
- // 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 master_keys = await this._rpc({route: "/vault/rights/get"});
- let 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}});
-
- // Re-encrypt the inboxes to not loose it
- const inbox_keys = await this._rpc({route: "/vault/inbox/get"});
- result = {};
- for (const uuid in inbox_keys) {
- result[uuid] = await utils.wrap(
- await utils.unwrap(inbox_keys[uuid], private_key),
- public_key
- );
- }
-
- await this._rpc({route: "/vault/inbox/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();
- if (!utils.supported()) return;
-
- 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
- );
- }
- }
- },
-
- _clickReencryptVault: async function () {
- await this._reencryptVault(false, true);
- },
-
- _clickVerifyVault: async function () {
- await this._reencryptVault(true, false);
- },
-
- /**
- * 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 {Boolean} verify
- * @param {Boolean} force
- */
- _reencryptVault: async function (verify = false, force = false) {
- const record = this.model.get(this.handle);
-
- await vault._ensure_keys();
-
- const self = this;
- const master_key = await utils.generate_key();
- const current_key = await vault.unwrap(record.data.master_key);
-
- // This stores the additional changes made to rights, fields, and files
- const changes = [];
- const problems = [];
-
- async function reencrypt(model, type) {
- // Load the entire data from the database
- const handle = await self.model.load({
- context: {vault_reencrypt: true},
- domain: [["vault_id", "=", record.res_id]],
- fields: ["iv", "value", "name", "entry_name"],
- modelName: model,
- limit: 0,
- type: "list",
- });
-
- const records = await self.model.get(handle, {raw: true});
- for (const rec of records.data) {
- if (!rec.data) continue;
-
- const d = rec.data;
- const val = await utils.sym_decrypt(current_key, d.value, d.iv);
- if (val === null) {
- problems.push(
- _.str.sprintf(
- _t("%s '%s' of entry '%s'"),
- type,
- d.name,
- d.entry_name
- )
- );
- continue;
- }
-
- const iv = utils.generate_iv_base64();
- const encrypted = await utils.sym_encrypt(master_key, val, iv);
-
- changes.push({
- id: rec.res_id,
- model: model,
- value: encrypted,
- iv: iv,
- });
- }
- }
-
- framework.blockUI();
- try {
- // Update the rights. Load without limit
- const right_handle = await this.model.load({
- domain: [["vault_id", "=", record.res_id]],
- fields: ["key"],
- modelName: "vault.right",
- limit: 0,
- type: "list",
- });
-
- const rights = await this.model.get(right_handle, {raw: true});
- for (const right of rights.data) {
- const key = await vault.wrap_with(
- master_key,
- right.data.public_key
- );
-
- changes.push({
- id: right.res_id,
- model: "vault.right",
- key: key,
- });
- }
-
- // Re-encrypt vault.field and vault.file
- await reencrypt("vault.field", "Field");
- await reencrypt("vault.file", "File");
-
- if (problems && !force) {
- framework.unblockUI();
-
- Dialog.alert(self, "", {
- title: _t("The following entries are broken:"),
- $content: $("
").html(problems.join(" \n")),
- });
- }
-
- if (!verify) {
- await this._rpc({
- route: "/vault/replace",
- params: {data: changes},
- });
- this.reload();
- }
- } finally {
- framework.unblockUI();
- }
- },
-
- /**
- * 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);
- },
-
- /**
- * 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);
-
- if (!utils.supported()) return result;
-
- 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/legacy/vault_widget.js b/vault/static/src/legacy/vault_widget.js
deleted file mode 100644
index c7478e6fa5..0000000000
--- a/vault/static/src/legacy/vault_widget.js
+++ /dev/null
@@ -1,563 +0,0 @@
-// © 2021 Florian Kantelberg - initOS GmbH
-// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-
-odoo.define("vault.fields", function (require) {
- "use strict";
-
- const core = require("web.core");
- const basic_fields = require("web.basic_fields");
- const download = require("web.download");
- const Exporter = require("vault.export");
- const registry = require("web.field_registry");
- const utils = require("vault.utils");
- const vault = require("vault");
-
- const _t = core._t;
- const QWeb = core.qweb;
-
- const VaultAbstract = {
- supported: function () {
- return utils.supported();
- },
- /**
- * 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;
-
- if (utils.supported()) {
- 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 () {
- if (!utils.supported()) return null;
-
- // 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 () {
- if (!utils.supported()) return null;
-
- // 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) {
- if (!utils.supported()) return null;
-
- 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) {
- if (!utils.supported()) return null;
-
- const iv = this._getIV();
- const key = await this._getMasterKey();
- return await utils.sym_encrypt(key, data, iv);
- },
- };
-
- // Basic field widget of the vault
- const 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) {
- const self = this;
- 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);
-
- if (utils.supported()) {
- 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
- const 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 && utils.supported()) {
- 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
- const 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 && utils.supported()) {
- 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();
- }
- },
- });
-
- const VaultInboxField = VaultField.extend({
- 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) {
- if (!utils.supported()) return null;
-
- 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
- const VaultInboxFile = VaultFile.extend({
- 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) {
- if (!utils.supported()) return null;
-
- 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/tests/test_controller.py b/vault/tests/test_controller.py
index adb0f5c947..21e06d2cf9 100644
--- a/vault/tests/test_controller.py
+++ b/vault/tests/test_controller.py
@@ -1,6 +1,7 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import json
import logging
from unittest.mock import MagicMock
@@ -15,19 +16,20 @@
class TestController(TransactionCase):
- def setUp(self):
- super().setUp()
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
- self.controller = main.Controller()
+ cls.controller = main.Controller()
- self.user = self.env["res.users"].create(
+ cls.user = cls.env["res.users"].create(
{"login": "test", "email": "test@test", "name": "test"}
)
- self.user.inbox_token = "42"
- self.user.keys.current = False
- self.key = self.env["res.users.key"].create(
+ cls.user.inbox_token = "42"
+ cls.user.keys.current = False
+ cls.key = cls.env["res.users.key"].create(
{
- "user_id": self.user.id,
+ "user_id": cls.user.id,
"public": "a public key",
"salt": "42",
"iv": "2424",
@@ -36,9 +38,9 @@ def setUp(self):
"current": True,
}
)
- self.inbox = self.env["vault.inbox"].create(
+ cls.inbox = cls.env["vault.inbox"].create(
{
- "user_id": self.user.id,
+ "user_id": cls.user.id,
"name": "Inbox",
"key": "4",
"iv": "1",
@@ -52,35 +54,38 @@ def setUp(self):
def test_vault_inbox(self):
def return_context(template, context):
self.assertEqual(template, "vault.inbox")
- return context
+ return json.dumps(context)
+
+ def load(response):
+ return json.loads(response.data)
with MockRequest(self.env) as request_mock:
request_mock.render = return_context
- response = self.controller.vault_inbox("")
+ response = load(self.controller.vault_inbox(""))
self.assertIn("error", response)
- response = self.controller.vault_inbox(self.user.inbox_token)
+ response = load(self.controller.vault_inbox(self.user.inbox_token))
self.assertNotIn("error", response)
self.assertEqual(response["public"], self.user.active_key.public)
# Try to eliminate each error step by step
request_mock.httprequest.method = "POST"
request_mock.params = {}
- response = self.controller.vault_inbox(self.user.inbox_token)
+ response = load(self.controller.vault_inbox(self.user.inbox_token))
self.assertIn("error", response)
request_mock.params["name"] = "test"
- response = self.controller.vault_inbox(self.user.inbox_token)
+ response = load(self.controller.vault_inbox(self.user.inbox_token))
self.assertIn("error", response)
request_mock.params.update(
{"encrypted": "secret", "encrypted_file": "file"}
)
- response = self.controller.vault_inbox(self.user.inbox_token)
+ response = load(self.controller.vault_inbox(self.user.inbox_token))
self.assertIn("error", response)
request_mock.params["filename"] = "filename"
- response = self.controller.vault_inbox(self.user.inbox_token)
+ response = load(self.controller.vault_inbox(self.user.inbox_token))
self.assertIn("error", response)
self.assertEqual(self.inbox.secret, "old secret")
@@ -88,14 +93,14 @@ def return_context(template, context):
# Store something successfully
request_mock.params.update({"iv": "iv", "key": "key"})
- response = self.controller.vault_inbox(self.inbox.token)
+ response = load(self.controller.vault_inbox(self.inbox.token))
self.assertNotIn("error", response)
self.assertEqual(self.inbox.secret, "secret")
self.assertEqual(self.inbox.secret_file, b"file")
# Test a duplicate inbox
self.inbox.copy().token = self.inbox.token
- response = self.controller.vault_inbox(self.inbox.token)
+ response = load(self.controller.vault_inbox(self.inbox.token))
self.assertIn("error", response)
def raise_error(*args, **kwargs):
@@ -105,7 +110,7 @@ def raise_error(*args, **kwargs):
try:
request_mock.httprequest.remote_addr = "127.0.0.1"
self.env["vault.inbox"]._patch_method("store_in_inbox", raise_error)
- response = self.controller.vault_inbox(self.user.inbox_token)
+ response = load(self.controller.vault_inbox(self.user.inbox_token))
finally:
self.env["vault.inbox"]._revert_method("store_in_inbox")
diff --git a/vault/tests/test_widgets.py b/vault/tests/test_widgets.py
index 3c9d3e871d..3d7d26dda3 100644
--- a/vault/tests/test_widgets.py
+++ b/vault/tests/test_widgets.py
@@ -133,7 +133,7 @@ def test_export(self):
wiz = self.env["vault.export.wizard"].create({"vault_id": self.vault.id})
# Export without entry should export entire vault
- wiz._change_content()
+ wiz._onchange_content()
entries = json.loads(wiz.content)
self.assertEqual({e["uuid"] for e in entries}, {second.uuid, self.entry.uuid})
self.assertEqual(len(entries), 2)
@@ -141,7 +141,7 @@ def test_export(self):
wiz.entry_id = self.entry
# Export the entire tree
- wiz._change_content()
+ wiz._onchange_content()
entries = json.loads(wiz.content)
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0]["uuid"], self.entry.uuid)
@@ -149,7 +149,7 @@ def test_export(self):
# Skip exporting childs
wiz.include_childs = False
- wiz._change_content()
+ wiz._onchange_content()
entries = json.loads(wiz.content)
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0]["uuid"], self.entry.uuid)
diff --git a/vault/views/res_users_views.xml b/vault/views/res_users_views.xml
index 11799f7d06..cbf44e6cd9 100644
--- a/vault/views/res_users_views.xml
+++ b/vault/views/res_users_views.xml
@@ -37,7 +37,7 @@
diff --git a/vault/views/vault_entry_views.xml b/vault/views/vault_entry_views.xml
index 738f520252..abe258eda5 100644
--- a/vault/views/vault_entry_views.xml
+++ b/vault/views/vault_entry_views.xml
@@ -88,7 +88,7 @@
diff --git a/vault/views/vault_inbox_views.xml b/vault/views/vault_inbox_views.xml
index 0dd4e293c9..9b9f1b0b90 100644
--- a/vault/views/vault_inbox_views.xml
+++ b/vault/views/vault_inbox_views.xml
@@ -13,7 +13,7 @@
vault.inbox
-