Skip to content

Commit

Permalink
Merge PR #254 into 14.0
Browse files Browse the repository at this point in the history
Signed-off-by dreispt
  • Loading branch information
OCA-git-bot committed Sep 21, 2021
2 parents a4aaa0f + eb705b4 commit cffc55c
Show file tree
Hide file tree
Showing 91 changed files with 6,717 additions and 0 deletions.
1 change: 1 addition & 0 deletions setup/vault/odoo/addons/vault
6 changes: 6 additions & 0 deletions setup/vault/setup.py
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
1 change: 1 addition & 0 deletions setup/vault_share/odoo/addons/vault_share
6 changes: 6 additions & 0 deletions setup/vault_share/setup.py
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
143 changes: 143 additions & 0 deletions 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 ┃
┃ ╚═════════════╝ ┃ ┗━━━━━━━━┛ ┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━┛
┗━━━━━━━━━━━━━━━━━┛
4 changes: 4 additions & 0 deletions 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
34 changes: 34 additions & 0 deletions 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"],
}
4 changes: 4 additions & 0 deletions 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
104 changes: 104 additions & 0 deletions 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/<string:token>", 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
17 changes: 17 additions & 0 deletions 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,
)
67 changes: 67 additions & 0 deletions 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")

0 comments on commit cffc55c

Please sign in to comment.