[fix] (armv7) cache.ExpireCache: remove option ENCRYPT_VALUE

Prophylactic encryption of the value currently makes no sense; on the contrary,
since the ``cryptography`` package is not available on armv7, it would cause
further problems.

Suggested-by: @dalf https://github.com/searxng/searxng/pull/4650#issuecomment-2830786661
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
Markus Heiser 2025-04-27 16:58:34 +02:00 committed by Markus Heiser
parent bdfe1c2a15
commit 7351c38e6c
2 changed files with 18 additions and 90 deletions

View File

@ -1,6 +1,5 @@
certifi==2025.4.26 certifi==2025.4.26
babel==2.17.0 babel==2.17.0
cryptography==44.0.2
flask-babel==4.0.0 flask-babel==4.0.0
flask==3.1.0 flask==3.1.0
jinja2==3.1.6 jinja2==3.1.6

View File

@ -16,21 +16,14 @@ import hashlib
import hmac import hmac
import os import os
import pickle import pickle
import secrets
import sqlite3 import sqlite3
import string import string
import tempfile import tempfile
import time import time
import typing import typing
from base64 import urlsafe_b64encode, urlsafe_b64decode
import msgspec import msgspec
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from searx import sqlitedb from searx import sqlitedb
from searx import logger from searx import logger
from searx import get_setting from searx import get_setting
@ -70,22 +63,15 @@ class ExpireCacheCfg(msgspec.Struct): # pylint: disable=too-few-public-methods
if required. if required.
""" """
# encryption of the values stored in the DB
password: bytes = get_setting("server.secret_key").encode() # type: ignore password: bytes = get_setting("server.secret_key").encode() # type: ignore
"""Password used in case of :py:obj:`ExpireCacheCfg.ENCRYPT_VALUE` is """Password used by :py:obj:`ExpireCache.secret_hash`.
``True``.
The default password is taken from :ref:`secret_key <server.secret_key>`. The default password is taken from :ref:`secret_key <server.secret_key>`.
When the password is changed, the values in the cache can no longer be When the password is changed, the hashed keys in the cache can no longer be
decrypted, which is why all values in the cache are deleted when the used, which is why all values in the cache are deleted when the password is
password is changed. changed.
""" """
ENCRYPT_VALUE: bool = True
"""Encrypting the values before they are written to the DB (see:
:py:obj:`ExpireCacheCfg.password`)."""
def __post_init__(self): def __post_init__(self):
# if db_url is unset, use a default DB in /tmp/sxng_cache_{name}.db # if db_url is unset, use a default DB in /tmp/sxng_cache_{name}.db
if not self.db_url: if not self.db_url:
@ -138,8 +124,7 @@ class ExpireCache(abc.ABC):
cfg: ExpireCacheCfg cfg: ExpireCacheCfg
hmac_iterations: int = 10_000 hash_token = "hash_token"
crypt_hash_property = "crypt_hash"
@abc.abstractmethod @abc.abstractmethod
def set(self, key: str, value: typing.Any, expire: int | None) -> bool: def set(self, key: str, value: typing.Any, expire: int | None) -> bool:
@ -154,16 +139,16 @@ class ExpireCache(abc.ABC):
"""Return *value* of *key*. If key is unset, ``None`` is returned.""" """Return *value* of *key*. If key is unset, ``None`` is returned."""
@abc.abstractmethod @abc.abstractmethod
def maintenance(self, force: bool = False, drop_crypted: bool = False) -> bool: def maintenance(self, force: bool = False, truncate: bool = False) -> bool:
"""Performs maintenance on the cache. """Performs maintenance on the cache.
``force``: ``force``:
Maintenance should be carried out even if the maintenance interval has Maintenance should be carried out even if the maintenance interval has
not yet been reached. not yet been reached.
``drop_crypted``: ``truncate``:
The encrypted values can no longer be decrypted (if the password is Truncate the entire cache, which is necessary, for example, if the
changed), they must be removed from the cache. password has changed.
""" """
@abc.abstractmethod @abc.abstractmethod
@ -191,68 +176,14 @@ class ExpireCache(abc.ABC):
_valid = "-_." + string.ascii_letters + string.digits _valid = "-_." + string.ascii_letters + string.digits
return "".join([c for c in name if c in _valid]) return "".join([c for c in name if c in _valid])
def derive_key(self, password: bytes, salt: bytes, iterations: int) -> bytes:
"""Derive a secret-key from a given password and salt."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=iterations,
)
return urlsafe_b64encode(kdf.derive(password))
def serialize(self, value: typing.Any) -> bytes: def serialize(self, value: typing.Any) -> bytes:
dump: bytes = pickle.dumps(value) dump: bytes = pickle.dumps(value)
if self.cfg.ENCRYPT_VALUE:
dump = self.encrypt(dump)
return dump return dump
def deserialize(self, value: bytes) -> typing.Any: def deserialize(self, value: bytes) -> typing.Any:
if self.cfg.ENCRYPT_VALUE:
value = self.decrypt(value)
obj = pickle.loads(value) obj = pickle.loads(value)
return obj return obj
def encrypt(self, message: bytes) -> bytes:
"""Encode and decode values by a method using `Fernet with password`_ where
the key is derived from the password (PBKDF2HMAC_). The *password* for
encryption is taken from the :ref:`server.secret_key`
.. _Fernet with password: https://stackoverflow.com/a/55147077
.. _PBKDF2HMAC: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#pbkdf2
"""
# Including the salt in the output makes it possible to use a random
# salt value, which in turn ensures the encrypted output is guaranteed
# to be fully random regardless of password reuse or message
# repetition.
salt = secrets.token_bytes(16) # randomly generated salt
# Including the iteration count ensures that you can adjust
# for CPU performance increases over time without losing the ability to
# decrypt older messages.
iterations = int(self.hmac_iterations)
key = self.derive_key(self.cfg.password, salt, iterations)
crypted_msg = Fernet(key).encrypt(message)
# Put salt and iteration count on the beginning of the binary
token = b"%b%b%b" % (salt, iterations.to_bytes(4, "big"), urlsafe_b64encode(crypted_msg))
return urlsafe_b64encode(token)
def decrypt(self, token: bytes) -> bytes:
token = urlsafe_b64decode(token)
# Strip salt and iteration count from the beginning of the binary
salt = token[:16]
iterations = int.from_bytes(token[16:20], "big")
key = self.derive_key(self.cfg.password, salt, iterations)
crypted_msg = urlsafe_b64decode(token[20:])
message = Fernet(key).decrypt(crypted_msg)
return message
def secret_hash(self, name: str | bytes) -> str: def secret_hash(self, name: str | bytes) -> str:
"""Creates a hash of the argument ``name``. The hash value is formed """Creates a hash of the argument ``name``. The hash value is formed
from the ``name`` combined with the :py:obj:`password from the ``name`` combined with the :py:obj:`password
@ -276,7 +207,6 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
- :py:obj:`ExpireCacheCfg.MAXHOLD_TIME` - :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`
- :py:obj:`ExpireCacheCfg.MAINTENANCE_PERIOD` - :py:obj:`ExpireCacheCfg.MAINTENANCE_PERIOD`
- :py:obj:`ExpireCacheCfg.MAINTENANCE_MODE` - :py:obj:`ExpireCacheCfg.MAINTENANCE_MODE`
- :py:obj:`ExpireCacheCfg.ENCRYPT_VALUE`
""" """
DB_SCHEMA = 1 DB_SCHEMA = 1
@ -300,18 +230,17 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
if not ret_val: if not ret_val:
return False return False
if self.cfg.ENCRYPT_VALUE:
new = hashlib.sha256(self.cfg.password).hexdigest() new = hashlib.sha256(self.cfg.password).hexdigest()
old = self.properties(self.crypt_hash_property) old = self.properties(self.hash_token)
if old != new: if old != new:
if old is not None: if old is not None:
log.warning("[%s] crypt token changed: drop all cache tables", self.cfg.name) log.warning("[%s] hash token changed: truncate all cache tables", self.cfg.name)
self.maintenance(force=True, drop_crypted=True) self.maintenance(force=True, truncate=True)
self.properties.set(self.crypt_hash_property, new) self.properties.set(self.hash_token, new)
return True return True
def maintenance(self, force: bool = False, drop_crypted: bool = False) -> bool: def maintenance(self, force: bool = False, truncate: bool = False) -> bool:
if not force and int(time.time()) < self.next_maintenance_time: if not force and int(time.time()) < self.next_maintenance_time:
# log.debug("no maintenance required yet, next maintenance interval is in the future") # log.debug("no maintenance required yet, next maintenance interval is in the future")
@ -321,7 +250,7 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
# (e.g. in multi thread or process environments). # (e.g. in multi thread or process environments).
self.properties.set("LAST_MAINTENANCE", "") # hint: this (also) sets the m_time of the property! self.properties.set("LAST_MAINTENANCE", "") # hint: this (also) sets the m_time of the property!
if drop_crypted: if truncate:
self.truncate_tables(self.table_names) self.truncate_tables(self.table_names)
return True return True