diff --git a/Readme.md b/Readme.md index 954cba5..5936d46 100644 --- a/Readme.md +++ b/Readme.md @@ -139,7 +139,8 @@ This backend uses [pickle](https://docs.python.org/3/library/pickle.html) module values, but the cashes can store values with md5-keyed hash. Use `secret` and `digestmod` parameters to protect your application from security vulnerabilities. -The `digestmod` is a hashing algorithm that can be used: `sum`, `md5` (default), `sha1` and `sha256` +The `digestmod` is a hashing algorithm that can be used: `sum`, `md5` (default), `sha1` and `sha256`. +To use [xxhash](https://pypi.org/project/xxhash/) algorithms (digestmods: `xxh3_64`, `xxh3_128`, `xxh32` and `xxh64`) install `xxhash` package or setup cashews with speedup requirements (`pip install cashews[speedup]`)) The `secret` is a salt for a hash. Pickle can't serialize any type of object. In case you need to store more complex types diff --git a/cashews/exceptions.py b/cashews/exceptions.py index 21e5623..8c907bd 100644 --- a/cashews/exceptions.py +++ b/cashews/exceptions.py @@ -14,6 +14,10 @@ class UnsupportedPicklerError(CacheError): """Unknown or unsupported pickle type.""" +class UnsupportedDigestmod(CacheError): + """Unknown or unsupported digestmod type.""" + + class UnsupportedCompressorError(CacheError): """Unknown or unsupported compress type.""" diff --git a/cashews/serialize.py b/cashews/serialize.py index 692d276..383dfbc 100644 --- a/cashews/serialize.py +++ b/cashews/serialize.py @@ -6,26 +6,58 @@ from typing import TYPE_CHECKING from .compresors import Compressor, CompressType, get_compressor -from .exceptions import DecompressionError, SignIsMissingError, UnSecureDataError +from .exceptions import DecompressionError, SignIsMissingError, UnSecureDataError, UnsupportedDigestmod from .picklers import Pickler, PicklerType, get_pickler +try: + import xxhash +except ImportError: + _XXHASH = False +else: + _XXHASH = True + + if TYPE_CHECKING: # pragma: no cover from ._typing import ICustomDecoder, ICustomEncoder, Key, Value from .backends.interface import Backend def _seal(digestmod): - def sign(key: bytes, value: bytes) -> bytes: - return hmac.new(key, value, digestmod).hexdigest().encode() + def sign(secret: bytes, value: bytes) -> bytes: + return hmac.new(secret, value, digestmod).hexdigest().encode() return sign -def simple_sign(key: bytes, value: bytes) -> bytes: - s = sum(key) + sum(value) +def simple_sign(secret: bytes, value: bytes) -> bytes: + s = sum(secret) + sum(value) return f"{s:x}".encode() +def _xxhash_xxh32_sign(secret: bytes, value: bytes) -> bytes: + if not _XXHASH: + raise UnsupportedDigestmod("xxhash is not installed") + return xxhash.xxh32_hexdigest(secret + value).encode() + + +def _xxhash_xxh64_sign(secret: bytes, value: bytes) -> bytes: + if not _XXHASH: + raise UnsupportedDigestmod("xxhash is not installed") + return xxhash.xxh64_hexdigest(secret + value).encode() + + +def _xxhash_xxh3_64_sign(secret: bytes, value: bytes) -> bytes: + if not _XXHASH: + raise UnsupportedDigestmod("xxhash is not installed") + return xxhash.xxh3_64_hexdigest(secret + value).encode() + + +def _xxhash_xxh3_128_sign(secret: bytes, value: bytes) -> bytes: + if not _XXHASH: + raise UnsupportedDigestmod("xxhash is not installed") + return xxhash.xxh3_128_hexdigest(secret + value).encode() + + def _to_bytes(value: str | bytes) -> bytes: if isinstance(value, str): value = value.encode() @@ -46,11 +78,17 @@ class HashSigner(Signer): b"md5": _seal(hashlib.md5), b"sha256": _seal(hashlib.sha256), b"sum": simple_sign, + b"xxh32": _xxhash_xxh32_sign, + b"xxh64": _xxhash_xxh64_sign, + b"xxh3_64": _xxhash_xxh3_64_sign, + b"xxh3_128": _xxhash_xxh3_128_sign, } def __init__(self, secret: str | bytes, digestmod: str | bytes = b"md5"): self._secret = _to_bytes(secret) self._digestmod = _to_bytes(digestmod) + if self._digestmod in (b"xxh32", b"xxh64") and not _XXHASH: + raise UnsupportedDigestmod() def sign(self, key: Key, value: bytes) -> bytes: sign = self._gen_sign(key, value, self._digestmod)