Skip to content

Commit ffe280f

Browse files
committed
[ADD][16.0] base_field_encrypted
1 parent d1c4792 commit ffe280f

File tree

19 files changed

+1262
-0
lines changed

19 files changed

+1262
-0
lines changed

base_field_encrypted/README.rst

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
====================
2+
Base Field Encrypted
3+
====================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:06e7f89cc87e56516e52403b660a0a674b99f951dc070feeb58654e05f89946c
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
20+
:target: https://github.com/OCA/server-auth/tree/16.0/base_field_encrypted
21+
:alt: OCA/server-auth
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-base_field_encrypted
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=16.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module provides a generic mixin to symmetrically encrypt data in the database
32+
while maintaining a standard Python workflow for developers.
33+
34+
Odoo natively handles `password="True"` on views by sending plaintext data
35+
to the client, where the browser masks it. This module intercepts reads and writes
36+
to implement actual "data at rest" encryption using the `cryptography` library.
37+
38+
**Table of contents**
39+
40+
.. contents::
41+
:local:
42+
43+
Configuration
44+
=============
45+
46+
To use this module, you need to configure a master encryption key in your
47+
``odoo.conf`` file:
48+
49+
1. Generate a URL-safe base64-encoded 32-byte key. You have two options:
50+
51+
**Option A (Recommended - UI Wizard):**
52+
- Log in as an Administrator (with "Settings" access).
53+
- Go to Settings > Technical > Security > Generate Encryption Key (Fernet).
54+
- Copy the generated key.
55+
56+
**Option B (Terminal):**
57+
.. code-block:: python
58+
59+
from cryptography.fernet import Fernet
60+
print(Fernet.generate_key().decode())
61+
62+
2. Add the copied key to your configuration file under the ``[options]`` section:
63+
64+
.. code-block:: ini
65+
66+
[options]
67+
encryption_key = <YOUR_GENERATED_KEY>
68+
69+
3. Restart your Odoo server.
70+
71+
If no key is configured, or the key is invalid, the module will log a warning
72+
and fallback to storing data in plaintext to prevent data loss.
73+
74+
**WARNING:** The encryption key is NOT stored in the database. If you lose
75+
the key, all previously encrypted fields will become permanently unreadable.
76+
Keep your ``odoo.conf`` safe.
77+
78+
Usage
79+
=====
80+
81+
To use the encryption capabilities in your own custom models:
82+
83+
1. Inherit the mixin in your model:
84+
85+
.. code-block:: python
86+
87+
class MyIntegration(models.Model):
88+
_name = 'my.integration'
89+
_inherit = ['encryption.mixin']
90+
91+
api_secret = fields.Char(string="API Secret", encrypted=True)
92+
93+
2. In your XML view, use the native `password="True"` attribute so the frontend masks it:
94+
95+
.. code-block:: xml
96+
97+
<field name="api_secret" password="True" />
98+
99+
Internal Python code can access `record.api_secret` normally and will receive the
100+
decrypted plaintext value. The web client will only receive `********`.
101+
102+
Bug Tracker
103+
===========
104+
105+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_.
106+
In case of trouble, please check there if your issue has already been reported.
107+
If you spotted it first, help us to smash it by providing a detailed and welcomed
108+
`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**>`_.
109+
110+
Do not contact contributors directly about support or help with technical issues.
111+
112+
Credits
113+
=======
114+
115+
Contributors
116+
~~~~~~~~~~~~
117+
118+
* Antonio Ruban <antoniodavid8@gmail.com>
119+
120+
Maintainers
121+
~~~~~~~~~~~
122+
123+
This module is maintained by the OCA.
124+
125+
.. image:: https://odoo-community.org/logo.png
126+
:alt: Odoo Community Association
127+
:target: https://odoo-community.org
128+
129+
OCA, or the Odoo Community Association, is a nonprofit organization whose
130+
mission is to support the collaborative development of Odoo features and
131+
promote its widespread use.
132+
133+
.. |maintainer-antoniodavid| image:: https://github.com/antoniodavid.png?size=40px
134+
:target: https://github.com/antoniodavid
135+
:alt: antoniodavid
136+
137+
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
138+
139+
|maintainer-antoniodavid|
140+
141+
This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/16.0/base_field_encrypted>`_ project on GitHub.
142+
143+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

base_field_encrypted/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import models
2+
from . import wizards
3+
from . import tools
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "Base Field Encrypted",
3+
"summary": "Symmetric encryption for fields in Odoo using cryptography (Fernet)",
4+
"version": "16.0.1.0.0",
5+
"category": "Tools",
6+
"author": "Odoo Community Association (OCA)",
7+
"website": "https://github.com/OCA/server-auth",
8+
"license": "AGPL-3",
9+
"depends": ["base"],
10+
"data": [
11+
"security/ir.model.access.csv",
12+
"wizards/generate_encryption_key_wizard_views.xml",
13+
],
14+
"maintainers": ["antoniodavid"],
15+
}

base_field_encrypted/i18n/es.po

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Translation of Odoo Server.
2+
# This file contains the translation of the following modules:
3+
# * base_field_encrypted
4+
#
5+
msgid ""
6+
msgstr ""
7+
"Project-Id-Version: Odoo Server 16.0\n"
8+
"Report-Msgid-Bugs-To: \n"
9+
"POT-Creation-Date: 2024-03-06 00:00+0000\n"
10+
"PO-Revision-Date: 2024-03-06 00:00+0000\n"
11+
"Last-Translator: \n"
12+
"Language-Team: \n"
13+
"MIME-Version: 1.0\n"
14+
"Content-Type: text/plain; charset=UTF-8\n"
15+
"Content-Transfer-Encoding: \n"
16+
"Plural-Forms: \n"
17+
18+
#. module: base_field_encrypted
19+
#: model:ir.model.fields,help:base_field_encrypted.field_generate_encryption_key_wizard__key
20+
msgid "Copy this key and paste it into your odoo.conf file. It will never be permanently saved in the database."
21+
msgstr "Copia esta llave y pégala en tu archivo odoo.conf. Jamás será guardada permanentemente en la base de datos."
22+
23+
#. module: base_field_encrypted
24+
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
25+
msgid "Danger!"
26+
msgstr "¡Peligro!"
27+
28+
#. module: base_field_encrypted
29+
#: model:ir.actions.act_window,name:base_field_encrypted.action_generate_encryption_key_wizard
30+
msgid "Generate Encryption Key"
31+
msgstr "Generar Llave de Encriptación"
32+
33+
#. module: base_field_encrypted
34+
#: model:ir.ui.menu,name:base_field_encrypted.menu_generate_encryption_key
35+
msgid "Generate Encryption Key (Fernet)"
36+
msgstr "Generar Llave Encriptación (Fernet)"
37+
38+
#. module: base_field_encrypted
39+
#: model:ir.model,name:base_field_encrypted.model_generate_encryption_key_wizard
40+
msgid "Generate Encryption Key Wizard"
41+
msgstr "Asistente para Generar Llave de Encriptación"
42+
43+
#. module: base_field_encrypted
44+
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
45+
msgid "Generate Another Key"
46+
msgstr "Generar Otra Llave"
47+
48+
#. module: base_field_encrypted
49+
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
50+
msgid "I've copied it"
51+
msgstr "Ya la copié"
52+
53+
#. module: base_field_encrypted
54+
#: model:ir.model.fields,field_description:base_field_encrypted.field_generate_encryption_key_wizard__message
55+
msgid "Instructions"
56+
msgstr "Instrucciones"
57+
58+
#. module: base_field_encrypted
59+
#: model:ir.model.fields,field_description:base_field_encrypted.field_generate_encryption_key_wizard__key
60+
msgid "New Encryption Key"
61+
msgstr "Nueva Llave de Encriptación"
62+
63+
#. module: base_field_encrypted
64+
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
65+
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."
66+
msgstr "Esta es tu única oportunidad de ver esta llave. Si cierras esta ventana sin copiarla, tendrás que generar una nueva."
67+
68+
#. module: base_field_encrypted
69+
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
70+
msgid "Encryption Key Generator"
71+
msgstr "Generador de Llaves de Encriptación"
72+
73+
#. module: base_field_encrypted
74+
#: model_terms:ir.ui.view,arch_db:base_field_encrypted.view_generate_encryption_key_wizard_form
75+
msgid ""
76+
"1. Copy the generated key above.\n"
77+
"2. Paste it into your odoo.conf file under the [options] section:\n"
78+
"\n"
79+
"[options]\n"
80+
"encryption_key = PASTE_THE_KEY_HERE\n"
81+
"\n"
82+
"3. Restart your Odoo server.\n"
83+
"\n"
84+
"⚠️ 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!"
85+
msgstr ""
86+
"1. Copia la llave generada arriba.\n"
87+
"2. Pégala en tu archivo odoo.conf debajo de la sección [options]:\n"
88+
"\n"
89+
"[options]\n"
90+
"encryption_key = PEGA_AQUÍ_LA_LLAVE\n"
91+
"\n"
92+
"3. Reinicia tu servidor Odoo.\n"
93+
"\n"
94+
"⚠️ 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!"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import encryption_mixin
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import logging
2+
import types
3+
4+
from odoo import api, models
5+
6+
from ..tools import crypto
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
11+
class EncryptionMixin(models.AbstractModel):
12+
_name = "encryption.mixin"
13+
_description = "Mixin to support encrypted fields"
14+
15+
@api.model
16+
def _valid_field_parameter(self, field, name):
17+
"""Tell Odoo that 'encrypted=True' is a valid parameter for fields on this model"""
18+
return name == "encrypted" or super()._valid_field_parameter(field, name)
19+
20+
@api.model
21+
def _get_encrypted_fields(self):
22+
"""Helper to retrieve all encrypted fields on the current model."""
23+
return [
24+
name
25+
for name, field in self._fields.items()
26+
if getattr(field, "encrypted", False)
27+
]
28+
29+
@api.model
30+
def _setup_complete(self):
31+
"""
32+
Dynamically patch the `convert_to_record` method of fields marked as `encrypted=True`.
33+
This is the most reliable hook in Odoo 16 to intercept data just before it is returned
34+
to the Python application from the cache.
35+
"""
36+
res = super()._setup_complete()
37+
38+
for name, field in self._fields.items():
39+
if getattr(field, "encrypted", False) and not getattr(
40+
field, "_encrypted_patched", False
41+
):
42+
_logger.info(
43+
"Patching encrypted field '%s' on model '%s'", name, self._name
44+
)
45+
46+
# Save original bound methods
47+
orig_convert_to_record = getattr(field, "convert_to_record")
48+
49+
def new_convert_to_record(self_field, value, record):
50+
# Standard Odoo conversion first
51+
val = orig_convert_to_record(value, record)
52+
# If it's an encrypted token, decrypt it before giving it to the python code
53+
if val and isinstance(val, str) and val.startswith("gAAAAAB"):
54+
return crypto.decrypt(val)
55+
return val
56+
57+
# Bind the new method to the field instance
58+
field.convert_to_record = types.MethodType(new_convert_to_record, field)
59+
field._encrypted_patched = True
60+
61+
return res
62+
63+
@api.model_create_multi
64+
def create(self, vals_list):
65+
"""Encrypt values on creation and prevent dummy value '********' from being saved."""
66+
encrypted_fields = self._get_encrypted_fields()
67+
if encrypted_fields:
68+
for vals in vals_list:
69+
for f in encrypted_fields:
70+
val = vals.get(f)
71+
if val == "********":
72+
del vals[f]
73+
elif val:
74+
vals[f] = crypto.encrypt(val)
75+
return super().create(vals_list)
76+
77+
def write(self, vals):
78+
"""Encrypt values on write and prevent dummy value '********' from overwriting."""
79+
encrypted_fields = self._get_encrypted_fields()
80+
if encrypted_fields:
81+
for f in encrypted_fields:
82+
val = vals.get(f)
83+
if val == "********":
84+
del vals[f]
85+
elif val:
86+
vals[f] = crypto.encrypt(val)
87+
if not vals:
88+
return True
89+
return super().write(vals)
90+
91+
def read(self, fields=None, load="_classic_read"):
92+
"""
93+
Intercept public reads (e.g., from the web client).
94+
Replace the plaintext cache values with the dummy mask '********'.
95+
"""
96+
res = super().read(fields, load)
97+
if not self.env.context.get("decrypt_fields"):
98+
encrypted_fields = self._get_encrypted_fields()
99+
if encrypted_fields:
100+
for rec in res:
101+
for f in encrypted_fields:
102+
if rec.get(f):
103+
rec[f] = "********"
104+
return res
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
To use this module, you need to configure a master encryption key in your
2+
``odoo.conf`` file:
3+
4+
1. Generate a URL-safe base64-encoded 32-byte key. You have two options:
5+
6+
**Option A (Recommended - UI Wizard):**
7+
- Log in as an Administrator (with "Settings" access).
8+
- Go to Settings > Technical > Security > Generate Encryption Key (Fernet).
9+
- Copy the generated key.
10+
11+
**Option B (Terminal):**
12+
.. code-block:: python
13+
14+
from cryptography.fernet import Fernet
15+
print(Fernet.generate_key().decode())
16+
17+
2. Add the copied key to your configuration file under the ``[options]`` section:
18+
19+
.. code-block:: ini
20+
21+
[options]
22+
encryption_key = <YOUR_GENERATED_KEY>
23+
24+
3. Restart your Odoo server.
25+
26+
If no key is configured, or the key is invalid, the module will log a warning
27+
and fallback to storing data in plaintext to prevent data loss.
28+
29+
**WARNING:** The encryption key is NOT stored in the database. If you lose
30+
the key, all previously encrypted fields will become permanently unreadable.
31+
Keep your ``odoo.conf`` safe.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Antonio Ruban <antoniodavid8@gmail.com>

0 commit comments

Comments
 (0)