Skip to content

Commit ce79726

Browse files
author
Fabien Coelho
committed
add optional checksum
1 parent f131e50 commit ce79726

File tree

4 files changed

+44
-23
lines changed

4 files changed

+44
-23
lines changed

CacheToolsUtils.py

+24-12
Original file line numberDiff line numberDiff line change
@@ -374,45 +374,57 @@ class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
374374
"""Encrypted Bytes Key-Value Cache.
375375
376376
:param secret: bytes of secret, at least 16 bytes.
377-
:param hsize: size of hashed key, default is 16.
377+
:param hsize: size of hashed key, default is *16*.
378+
:param csize: value checksum size, default is *0*.
378379
379380
The key is *not* encrypted but simply hashed, thus they are
380381
fixed size with a very low collision probability.
381382
382383
By design, the clear-text key is needed to recover the value,
383-
as each value is encrypted with its own key.
384-
385-
There is no integrity check on the value.
384+
as each value is encrypted with its own key and nonce.
386385
387386
Algorithms:
388-
- SHA3: hash/key/nonce derivation.
387+
- SHA3: hash/key/nonce derivation and checksum.
389388
- Salsa20: value encryption.
390389
"""
391390

392-
def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16):
391+
def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16, csize: int = 0):
393392
self._cache = cache
394393
assert len(secret) >= 16
395394
self._secret = secret
396-
assert 8 <= hsize <= 24
395+
assert 8 <= hsize <= 32
397396
self._hsize = hsize
397+
assert 0 <= csize <= 32
398+
self._csize = csize
398399
from Crypto.Cipher import Salsa20
399400
self._cipher = Salsa20
400401

401-
def _keydev(self, key):
402+
def _keydev(self, key) -> tuple[bytes, bytes, bytes]:
403+
"""Compute hash, key and nonce from initial key."""
402404
hkey = hashlib.sha3_512(key + self._secret).digest()
403-
sz = self._hsize
404-
return (hkey[:sz], hkey[sz:sz+32], hkey[sz+32:sz+40])
405+
# NOTE hash and nonce may overlap, which is not an issue
406+
return (hkey[:self._hsize], hkey[32:], hkey[24:32])
405407

406408
def _key(self, key):
407409
return self._keydev(key)[0]
408410

409411
def __setitem__(self, key, val):
410412
hkey, vkey, vnonce = self._keydev(key)
411-
self._cache[hkey] = self._cipher.new(key=vkey, nonce=vnonce).encrypt(val)
413+
xval = self._cipher.new(key=vkey, nonce=vnonce).encrypt(val)
414+
if self._csize:
415+
cs = hashlib.sha3_256(val).digest()[:self._csize]
416+
xval = cs + xval
417+
self._cache[hkey] = xval
412418

413419
def __getitem__(self, key):
414420
hkey, vkey, vnonce = self._keydev(key)
415-
return self._cipher.new(key=vkey, nonce=vnonce).decrypt(self._cache[hkey])
421+
xval = self._cache[hkey]
422+
if self._csize: # split cs from encrypted value
423+
cs, xval = xval[:self._csize], xval[self._csize:]
424+
val = self._cipher.new(key=vkey, nonce=vnonce).decrypt(xval)
425+
if self._csize and cs != hashlib.sha3_256(val).digest()[:self._csize]:
426+
raise KeyError(f"invalid encrypted value for key {key}")
427+
return val
416428

417429

418430
class BytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):

docs/REFERENCE.md

+6-8
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,11 @@ Hashing is based on _SHA3_, encryption uses _Salsa20_.
9191
Because of the stream cipher the value length is somehow leaked.
9292

9393
```python
94-
cache = EncryptedCache(actual_cache, secret=b"super secret stuff you cannot guess", hsize=16)
94+
cache = EncryptedCache(actual_cache, secret=b"super secret stuff you cannot guess", hsize=16, csize=0)
9595
```
9696

97-
Hash size $s$ can be extended up to _24_, key collision probability is $2^{-4 s}$.
97+
Hash size `hsize` can be extended up to _32_, key collision probability is $2^{-4 s}}$.
98+
An optional value checksum can be triggered by setting `csize`.
9899

99100
The point of this class is to bring security to cached data on distributed
100101
systems such as Redis. There is no much point to encrypting in-memory caches.
@@ -117,13 +118,10 @@ def foo(what, ever):
117118
return
118119
```
119120

120-
## ToBytesCache
121+
## ToBytesCache and BytesCache
121122

122-
Map keys and values to bytes.
123-
124-
## BytesCache
125-
126-
Handle bytes keys and values and map them to strings.
123+
Map keys and values to bytes, or
124+
handle bytes keys and values and map them to strings.
127125

128126
## MemCached
129127

docs/VERSIONS.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ Install [package](https://pypi.org/project/CacheToolsUtils/) from
1919
Maybe the existing client can do that with appropriate options?
2020
- `cached`: add `contains` and `delete` parameters to change names?
2121
- I cannot say that all this is clear wrt `str` vs `bytes` vs whatever…
22-
- add integrity check to `EncryptedCache`.
22+
- allow to change encryption algorithm?
2323

2424
## ? on ?
2525

2626
Code cleanup.
27+
Add optional integrity check to `EncryptedCache`.
2728

2829
## 10.0 on 2024-12-07
2930

test.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def test_redis():
246246
c0.flushdb()
247247

248248
c1 = ctu.RedisCache(c0, raw=True)
249-
c2 = ctu.EncryptedCache(c1, SECRET, hsize=24)
249+
c2 = ctu.EncryptedCache(c1, SECRET, hsize=20, csize=4)
250250
c3 = ctu.ToBytesCache(c2)
251251
c4 = ctu.DebugCache(c3, log)
252252
cache = ctu.LockedCache(c4, threading.RLock())
@@ -563,10 +563,20 @@ def test_cache_key():
563563

564564
def test_encrypted_cache():
565565
# bytes
566-
cache = ctu.EncryptedCache(ctu.DictCache(), SECRET)
566+
actual = ctu.DictCache()
567+
cache = ctu.EncryptedCache(actual, SECRET, csize=1)
567568
cache[b"Hello"] = b"World!"
568569
assert b"Hello" in cache
569570
assert cache[b"Hello"] == b"World!"
571+
# bad checksum
572+
assert len(actual) == 1
573+
k = list(actual.keys())[0]
574+
actual[k] = bytes([(actual[k][0] + 42) % 256]) + actual[k][1:]
575+
try:
576+
_ = cache[b"Hello"]
577+
pytest.fail("must raise an exception")
578+
except KeyError as ke:
579+
assert "invalid encrypted value" in str(ke)
570580
del cache[b"Hello"]
571581
assert b"Hello" not in cache
572582
# strings

0 commit comments

Comments
 (0)