Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions base_field_encrypted/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
====================
Base Field Encrypted
====================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:06e7f89cc87e56516e52403b660a0a674b99f951dc070feeb58654e05f89946c
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
:target: https://github.com/OCA/server-auth/tree/16.0/base_field_encrypted
: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-16-0/server-auth-16-0-base_field_encrypted
: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=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module provides a generic mixin to symmetrically encrypt data in the database
while maintaining a standard Python workflow for developers.

Odoo natively handles `password="True"` on views by sending plaintext data
to the client, where the browser masks it. This module intercepts reads and writes
to implement actual "data at rest" encryption using the `cryptography` library.

**Table of contents**

.. contents::
:local:

Configuration
=============

To use this module, you need to configure a master encryption key in your
``odoo.conf`` file:

1. Generate a URL-safe base64-encoded 32-byte key. You have two options:

**Option A (Recommended - UI Wizard):**
- Log in as an Administrator (with "Settings" access).
- Go to Settings > Technical > Security > Generate Encryption Key (Fernet).
- Copy the generated key.

**Option B (Terminal):**
.. code-block:: python

from cryptography.fernet import Fernet
print(Fernet.generate_key().decode())

2. Add the copied key to your configuration file under the ``[options]`` section:

.. code-block:: ini

[options]
encryption_key = <YOUR_GENERATED_KEY>

3. Restart your Odoo server.

If no key is configured, or the key is invalid, the module will log a warning
and fallback to storing data in plaintext to prevent data loss.

**WARNING:** The encryption key is NOT stored in the database. If you lose
the key, all previously encrypted fields will become permanently unreadable.
Keep your ``odoo.conf`` safe.

Usage
=====

To use the encryption capabilities in your own custom models:

1. Inherit the mixin in your model:

.. code-block:: python

class MyIntegration(models.Model):
_name = 'my.integration'
_inherit = ['encryption.mixin']

api_secret = fields.Char(string="API Secret", encrypted=True)

2. In your XML view, use the native `password="True"` attribute so the frontend masks it:

.. code-block:: xml

<field name="api_secret" password="True" />

Internal Python code can access `record.api_secret` normally and will receive the
decrypted plaintext value. The web client will only receive `********`.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/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 <https://github.com/OCA/server-auth/issues/new?body=module:%20base_field_encrypted%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Contributors
~~~~~~~~~~~~

* Antonio Ruban <antoniodavid8@gmail.com>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

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

.. |maintainer-antoniodavid| image:: https://github.com/antoniodavid.png?size=40px
:target: https://github.com/antoniodavid
:alt: antoniodavid

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-antoniodavid|

This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/16.0/base_field_encrypted>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
3 changes: 3 additions & 0 deletions base_field_encrypted/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import models
from . import wizards
from . import tools
15 changes: 15 additions & 0 deletions base_field_encrypted/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Base Field Encrypted",
"summary": "Symmetric encryption for fields in Odoo using cryptography (Fernet)",
"version": "16.0.1.0.0",
"category": "Tools",
"author": "Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-auth",
"license": "AGPL-3",
"depends": ["base"],
"data": [
"security/ir.model.access.csv",
"wizards/generate_encryption_key_wizard_views.xml",
],
"maintainers": ["antoniodavid"],
}
89 changes: 89 additions & 0 deletions base_field_encrypted/i18n/es.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_field_encrypted
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-06 00:00+0000\n"
"PO-Revision-Date: 2024-03-06 00:00+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: base_field_encrypted
#: model:ir.model.fields,help:base_field_encrypted.field_generate_encryption_key_wizard__key
msgid "Copy this key and paste it into your odoo.conf file. It will never be permanently saved in the database."
msgstr "Copia esta llave y pégala en tu archivo odoo.conf. Jamás será guardada permanentemente en la base de datos."

#. module: base_field_encrypted
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
msgid "Danger!"
msgstr "¡Peligro!"

#. module: base_field_encrypted
#: model:ir.actions.act_window,name:base_field_encrypted.action_generate_encryption_key_wizard
msgid "Generate Encryption Key"
msgstr "Generar Llave de Encriptación"

#. module: base_field_encrypted
#: model:ir.ui.menu,name:base_field_encrypted.menu_generate_encryption_key
msgid "Generate Encryption Key (Fernet)"
msgstr "Generar Llave Encriptación (Fernet)"

#. module: base_field_encrypted
#: model:ir.model,name:base_field_encrypted.model_generate_encryption_key_wizard
msgid "Generate Encryption Key Wizard"
msgstr "Asistente para Generar Llave de Encriptación"

#. module: base_field_encrypted
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
msgid "Generate Another Key"
msgstr "Generar Otra Llave"

#. module: base_field_encrypted
#: model:ir.model.fields,field_description:base_field_encrypted.field_generate_encryption_key_wizard__message
msgid "Instructions"
msgstr "Instrucciones"

#. module: base_field_encrypted
#: model:ir.model.fields,field_description:base_field_encrypted.field_generate_encryption_key_wizard__key
msgid "New Encryption Key"
msgstr "Nueva Llave de Encriptación"

#. module: base_field_encrypted
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
msgid "This is your only chance to see this key. If you close this window without copying it, you will have to generate a new one."
msgstr "Esta es tu única oportunidad de ver esta llave. Si cierras esta ventana sin copiarla, tendrás que generar una nueva."

#. module: base_field_encrypted
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
msgid "Encryption Key Generator"
msgstr "Generador de Llaves de Encriptación"

#. module: base_field_encrypted
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
msgid ""
"1. Copy the generated key above.\n"
"2. Paste it into your odoo.conf file under the [options] section:\n"
"\n"
"[options]\n"
"encryption_key = PASTE_THE_KEY_HERE\n"
"\n"
"3. Restart your Odoo server.\n"
"\n"
"⚠️ IMPORTANT: This key is NOT saved in the database for security reasons. If you lose it and had encrypted data, that data will be lost forever!"
msgstr ""
"1. Copia la llave generada arriba.\n"
"2. Pégala en tu archivo odoo.conf debajo de la sección [options]:\n"
"\n"
"[options]\n"
"encryption_key = PEGA_AQUÍ_LA_LLAVE\n"
"\n"
"3. Reinicia tu servidor Odoo.\n"
"\n"
"⚠️ IMPORTANTE: Esta llave NO se guarda en la base de datos por seguridad. ¡Si la pierdes y tenías datos encriptados, se perderán para siempre!"
1 change: 1 addition & 0 deletions base_field_encrypted/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import encryption_mixin
106 changes: 106 additions & 0 deletions base_field_encrypted/models/encryption_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import logging
import types

from odoo import api, models

from ..tools import crypto

_logger = logging.getLogger(__name__)


class EncryptionMixin(models.AbstractModel):
_name = "encryption.mixin"
_description = "Mixin to support encrypted fields"

@api.model
def _valid_field_parameter(self, field, name):
"""Tell Odoo that 'encrypted=True' is a valid parameter for fields on this model"""
return name == "encrypted" or super()._valid_field_parameter(field, name)

@api.model
def _get_encrypted_fields(self):
"""Helper to retrieve all encrypted fields on the current model."""
return [
name
for name, field in self._fields.items()
if getattr(field, "encrypted", False)
]

@api.model
def _setup_complete(self):
"""
Dynamically patch the `convert_to_record` method of fields marked as `encrypted=True`.
This is the most reliable hook in Odoo 16 to intercept data just before it is returned
to the Python application from the cache.
"""
res = super()._setup_complete()

for name, field in self._fields.items():
if getattr(field, "encrypted", False) and not getattr(
field, "_encrypted_patched", False
):
_logger.info(
"Patching encrypted field '%s' on model '%s'", name, self._name
)

# Save original bound methods
orig_convert_to_record = field.convert_to_record

def new_convert_to_record(
self_field, value, record, _orig=orig_convert_to_record
):
# Standard Odoo conversion first
val = _orig(value, record)
# If it's an encrypted token, decrypt it before giving it to the python code
if val and isinstance(val, str) and val.startswith("gAAAAAB"):
return crypto.decrypt(val)
return val

# Bind the new method to the field instance
field.convert_to_record = types.MethodType(new_convert_to_record, field)
field._encrypted_patched = True

return res

@api.model_create_multi
def create(self, vals_list):
"""Encrypt values on creation and prevent dummy value '********' from being saved."""
encrypted_fields = self._get_encrypted_fields()
if encrypted_fields:
for vals in vals_list:
for f in encrypted_fields:
val = vals.get(f)
if val == "********":
del vals[f]
elif val:
vals[f] = crypto.encrypt(val)
return super().create(vals_list)

def write(self, vals):
"""Encrypt values on write and prevent dummy value '********' from overwriting."""
encrypted_fields = self._get_encrypted_fields()
if encrypted_fields:
for f in encrypted_fields:
val = vals.get(f)
if val == "********":
del vals[f]
elif val:
vals[f] = crypto.encrypt(val)
if not vals:
return True
return super().write(vals)

def read(self, fields=None, load="_classic_read"):
"""
Intercept public reads (e.g., from the web client).
Replace the plaintext cache values with the dummy mask '********'.
"""
res = super().read(fields, load)
if not self.env.context.get("decrypt_fields"):
encrypted_fields = self._get_encrypted_fields()
if encrypted_fields:
for rec in res:
for f in encrypted_fields:
if rec.get(f):
rec[f] = "********"
return res
31 changes: 31 additions & 0 deletions base_field_encrypted/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
To use this module, you need to configure a master encryption key in your
``odoo.conf`` file:

1. Generate a URL-safe base64-encoded 32-byte key. You have two options:

**Option A (Recommended - UI Wizard):**
- Log in as an Administrator (with "Settings" access).
- Go to Settings > Technical > Security > Generate Encryption Key (Fernet).
- Copy the generated key.

**Option B (Terminal):**
.. code-block:: python

from cryptography.fernet import Fernet
print(Fernet.generate_key().decode())

2. Add the copied key to your configuration file under the ``[options]`` section:

.. code-block:: ini

[options]
encryption_key = <YOUR_GENERATED_KEY>

3. Restart your Odoo server.

If no key is configured, or the key is invalid, the module will log a warning
and fallback to storing data in plaintext to prevent data loss.

**WARNING:** The encryption key is NOT stored in the database. If you lose
the key, all previously encrypted fields will become permanently unreadable.
Keep your ``odoo.conf`` safe.
1 change: 1 addition & 0 deletions base_field_encrypted/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Antonio Ruban <antoniodavid8@gmail.com>
Loading
Loading