CacheToolsUtils 10.0__tar.gz → 10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/CacheToolsUtils.egg-info/PKG-INFO +2 -1
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/CacheToolsUtils.py +152 -97
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/PKG-INFO +2 -1
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/README.md +1 -0
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/pyproject.toml +1 -1
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/CacheToolsUtils.egg-info/SOURCES.txt +0 -0
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/CacheToolsUtils.egg-info/dependency_links.txt +0 -0
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/CacheToolsUtils.egg-info/requires.txt +0 -0
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/CacheToolsUtils.egg-info/top_level.txt +0 -0
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/LICENSE +0 -0
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/MANIFEST.in +0 -0
- {cachetoolsutils-10.0 → cachetoolsutils-10.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: CacheToolsUtils
|
|
3
|
-
Version: 10.
|
|
3
|
+
Version: 10.2
|
|
4
4
|
Summary: Cachetools Utilities
|
|
5
5
|
Author-email: Fabien Coelho <cachetools.utils@coelho.net>
|
|
6
6
|
License: CC0
|
|
@@ -89,6 +89,7 @@ with `cachetools`.
|
|
|
89
89
|
- `DebugCache` to trace cache calls using `logging`.
|
|
90
90
|
- `EncryptedCache` a cache with key hashing and value encryption.
|
|
91
91
|
- `ToBytesCache` map keys and values to bytes.
|
|
92
|
+
- `BytesCache` map bytes keys and values to strings.
|
|
92
93
|
|
|
93
94
|
### Cache utilities
|
|
94
95
|
|
|
@@ -367,6 +367,147 @@ class TwoLevelCache(_MutMapMix, MutableMapping):
|
|
|
367
367
|
self._cache2.reset() # type: ignore
|
|
368
368
|
|
|
369
369
|
|
|
370
|
+
# NOTE this is quite costly, a Cipher object is created to encrypt/decrypt each value.
|
|
371
|
+
# another design could keep a persistant cipher.
|
|
372
|
+
class _Cipher:
|
|
373
|
+
"""Not so convincing internal class to abstract cipher algorithms."""
|
|
374
|
+
|
|
375
|
+
def __init__(self, name):
|
|
376
|
+
self._name = name
|
|
377
|
+
if name == "Salsa20":
|
|
378
|
+
from Crypto.Cipher import Salsa20 as cipher
|
|
379
|
+
# NOTE hash and nonce may overlap, which is not an issue
|
|
380
|
+
params = {"key": (32, 64), "nonce": (24, 32)}
|
|
381
|
+
kwargs = {}
|
|
382
|
+
size = 0
|
|
383
|
+
elif name == "ChaCha20":
|
|
384
|
+
from Crypto.Cipher import ChaCha20 as cipher
|
|
385
|
+
# NOTE hash and nonce may overlap, which is not an issue
|
|
386
|
+
params = {"key": (32, 64), "nonce": (24, 32)}
|
|
387
|
+
kwargs = {}
|
|
388
|
+
size = 0
|
|
389
|
+
elif name in ("AES", "AES-128", "AES-128-CBC"):
|
|
390
|
+
from Crypto.Cipher import AES as cipher
|
|
391
|
+
params = {"key": (48, 64), "iv": (32, 48)}
|
|
392
|
+
kwargs = {"mode": cipher.MODE_CBC}
|
|
393
|
+
size = 16
|
|
394
|
+
else:
|
|
395
|
+
raise Exception(f"unexpected cipher: {name}")
|
|
396
|
+
self._cipher = cipher
|
|
397
|
+
self._params = params
|
|
398
|
+
self._kwargs = kwargs
|
|
399
|
+
if size:
|
|
400
|
+
from Crypto.Util.Padding import pad, unpad
|
|
401
|
+
self._pad = lambda s: pad(s, size)
|
|
402
|
+
self._unpad = lambda s: unpad(s, size)
|
|
403
|
+
else:
|
|
404
|
+
self._pad = lambda s: s
|
|
405
|
+
self._unpad = lambda s: s
|
|
406
|
+
|
|
407
|
+
def new(self, derived: bytes):
|
|
408
|
+
kwargs = {k: derived[v[0]: v[1]] for k, v in self._params.items()}
|
|
409
|
+
return self._cipher.new(**kwargs, **self._kwargs) # type: ignore
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
#
|
|
413
|
+
# Encrypted Cache
|
|
414
|
+
#
|
|
415
|
+
class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
416
|
+
"""Encrypted Bytes Key-Value Cache.
|
|
417
|
+
|
|
418
|
+
:param secret: bytes of secret, at least 16 bytes.
|
|
419
|
+
:param hsize: size of hashed key, default is *16*.
|
|
420
|
+
:param csize: value checksum size, default is *0*.
|
|
421
|
+
:param cipher: chose cipher from "Salsa20", "AES-128-CBC" or "ChaCha20".
|
|
422
|
+
|
|
423
|
+
The key is *not* encrypted but simply hashed, thus they are
|
|
424
|
+
fixed size with a very low collision probability.
|
|
425
|
+
|
|
426
|
+
By design, the clear-text key is needed to recover the value,
|
|
427
|
+
as each value is encrypted with its own key and nonce.
|
|
428
|
+
|
|
429
|
+
Algorithms:
|
|
430
|
+
|
|
431
|
+
- SHA3: hash/key/nonce derivation and checksum.
|
|
432
|
+
- Salsa20, AES-128-CBC or ChaCha20: value encryption.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16, csize: int = 0, cipher: str = "Salsa20"):
|
|
436
|
+
self._cache = cache
|
|
437
|
+
assert len(secret) >= 16
|
|
438
|
+
self._secret = secret
|
|
439
|
+
assert 8 <= hsize <= 32
|
|
440
|
+
self._hsize = hsize
|
|
441
|
+
assert 0 <= csize <= 32
|
|
442
|
+
self._csize = csize
|
|
443
|
+
self._cipher = _Cipher(cipher)
|
|
444
|
+
|
|
445
|
+
def _derive(self, key) -> tuple[bytes, bytes]:
|
|
446
|
+
"""Derive hash and stuff from initial key and secret."""
|
|
447
|
+
derived = hashlib.sha3_512(key + self._secret).digest()
|
|
448
|
+
return (derived[:self._hsize], derived)
|
|
449
|
+
|
|
450
|
+
def _key(self, key):
|
|
451
|
+
return self._derive(key)[0]
|
|
452
|
+
|
|
453
|
+
def __setitem__(self, key, val):
|
|
454
|
+
hkey, derived = self._derive(key)
|
|
455
|
+
xval = self._cipher.new(derived).encrypt(self._cipher._pad(val))
|
|
456
|
+
if self._csize:
|
|
457
|
+
xval = hashlib.sha3_256(val).digest()[:self._csize] + xval
|
|
458
|
+
self._cache[hkey] = xval
|
|
459
|
+
|
|
460
|
+
def __getitem__(self, key):
|
|
461
|
+
hkey, derived = self._derive(key)
|
|
462
|
+
xval = self._cache[hkey]
|
|
463
|
+
if self._csize: # split cs from encrypted value
|
|
464
|
+
cs, xval = xval[:self._csize], xval[self._csize:]
|
|
465
|
+
else:
|
|
466
|
+
cs = None
|
|
467
|
+
val = self._cipher._unpad(self._cipher.new(derived).decrypt(xval))
|
|
468
|
+
if self._csize and cs != hashlib.sha3_256(val).digest()[:self._csize]:
|
|
469
|
+
raise KeyError(f"invalid encrypted value for key {key}")
|
|
470
|
+
return val
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class BytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
474
|
+
"""Map bytes to strings."""
|
|
475
|
+
|
|
476
|
+
def __init__(self, cache):
|
|
477
|
+
self._cache = cache
|
|
478
|
+
|
|
479
|
+
def _key(self, key):
|
|
480
|
+
# assert isinstance(key, bytes)
|
|
481
|
+
return base64.b85encode(key).decode("ASCII")
|
|
482
|
+
|
|
483
|
+
def __setitem__(self, key, val):
|
|
484
|
+
self._cache.__setitem__(self._key(key), self._key(val))
|
|
485
|
+
|
|
486
|
+
def __getitem__(self, key):
|
|
487
|
+
val = self._cache.__getitem__(self._key(key))
|
|
488
|
+
# assert isinstance(val, str)
|
|
489
|
+
return base64.b85decode(val)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class ToBytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
493
|
+
"""Map (JSON-serializable) cache keys and values to bytes."""
|
|
494
|
+
|
|
495
|
+
def __init__(self, cache):
|
|
496
|
+
self._cache = cache
|
|
497
|
+
|
|
498
|
+
def _key(self, key):
|
|
499
|
+
return json.dumps(key, sort_keys=True, separators=(",", ":")).encode("UTF-8")
|
|
500
|
+
|
|
501
|
+
def __setitem__(self, key, val):
|
|
502
|
+
self._cache.__setitem__(self._key(key), self._key(val))
|
|
503
|
+
|
|
504
|
+
def __getitem__(self, key):
|
|
505
|
+
return json.loads(self._cache.__getitem__(self._key(key)).decode("UTF-8"))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
#
|
|
509
|
+
# CACHED DECORATOR AND HELPERS
|
|
510
|
+
#
|
|
370
511
|
def cached(cache, *args, **kwargs):
|
|
371
512
|
"""Extended decorator with delete and exists.
|
|
372
513
|
|
|
@@ -497,7 +638,9 @@ class _HashJsonKey:
|
|
|
497
638
|
return self._key
|
|
498
639
|
|
|
499
640
|
|
|
641
|
+
# NOTE not very useful, behaves as _HashJsonKey.
|
|
500
642
|
def hash_json_key(*args, **kwargs):
|
|
643
|
+
"""Function handling of above class, to keep it hidden."""
|
|
501
644
|
return _HashJsonKey(*args, **kwargs)
|
|
502
645
|
|
|
503
646
|
|
|
@@ -508,93 +651,9 @@ def full_hash_key(*args, **kwargs) -> str:
|
|
|
508
651
|
return base64.b85encode(hkey).decode("ASCII")
|
|
509
652
|
|
|
510
653
|
|
|
511
|
-
#
|
|
512
|
-
# Encrypted Cache
|
|
513
|
-
#
|
|
514
|
-
class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
515
|
-
"""Encrypted Bytes Key-Value Cache.
|
|
516
|
-
|
|
517
|
-
:param secret: bytes of secret, at least 16 bytes.
|
|
518
|
-
:param hsize: size of hashed key, default is 16.
|
|
519
|
-
|
|
520
|
-
The key is *not* encrypted but simply hashed, thus they are
|
|
521
|
-
fixed size with a very low collision probability.
|
|
522
|
-
|
|
523
|
-
By design, the clear-text key is needed to recover the value,
|
|
524
|
-
as each value is encrypted with its own key.
|
|
525
|
-
|
|
526
|
-
There is no integrity check on the value.
|
|
527
|
-
|
|
528
|
-
Algorithms:
|
|
529
|
-
- SHA3: hash/key/nonce derivation.
|
|
530
|
-
- Salsa20: value encryption.
|
|
531
|
-
"""
|
|
532
|
-
|
|
533
|
-
def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16):
|
|
534
|
-
self._cache = cache
|
|
535
|
-
assert len(secret) >= 16
|
|
536
|
-
self._secret = secret
|
|
537
|
-
assert 8 <= hsize <= 24
|
|
538
|
-
self._hsize = hsize
|
|
539
|
-
from Crypto.Cipher import Salsa20
|
|
540
|
-
self._cipher = Salsa20
|
|
541
|
-
|
|
542
|
-
def _keydev(self, key):
|
|
543
|
-
hkey = hashlib.sha3_512(key + self._secret).digest()
|
|
544
|
-
sz = self._hsize
|
|
545
|
-
return (hkey[:sz], hkey[sz:sz+32], hkey[sz+32:sz+40])
|
|
546
|
-
|
|
547
|
-
def _key(self, key):
|
|
548
|
-
return self._keydev(key)[0]
|
|
549
|
-
|
|
550
|
-
def __setitem__(self, key, val):
|
|
551
|
-
hkey, vkey, vnonce = self._keydev(key)
|
|
552
|
-
self._cache[hkey] = self._cipher.new(key=vkey, nonce=vnonce).encrypt(val)
|
|
553
|
-
|
|
554
|
-
def __getitem__(self, key):
|
|
555
|
-
hkey, vkey, vnonce = self._keydev(key)
|
|
556
|
-
return self._cipher.new(key=vkey, nonce=vnonce).decrypt(self._cache[hkey])
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
class BytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
560
|
-
"""Map bytes to strings."""
|
|
561
|
-
|
|
562
|
-
def __init__(self, cache):
|
|
563
|
-
self._cache = cache
|
|
564
|
-
|
|
565
|
-
def _key(self, key):
|
|
566
|
-
# assert isinstance(key, bytes)
|
|
567
|
-
return base64.b85encode(key).decode("ASCII")
|
|
568
|
-
|
|
569
|
-
def __setitem__(self, key, val):
|
|
570
|
-
self._cache.__setitem__(self._key(key), self._key(val))
|
|
571
|
-
|
|
572
|
-
def __getitem__(self, key):
|
|
573
|
-
val = self._cache.__getitem__(self._key(key))
|
|
574
|
-
# assert isinstance(val, str)
|
|
575
|
-
return base64.b85decode(val)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
class ToBytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
579
|
-
"""Map (JSON-serializable) cache keys and values to bytes."""
|
|
580
|
-
|
|
581
|
-
def __init__(self, cache):
|
|
582
|
-
self._cache = cache
|
|
583
|
-
|
|
584
|
-
def _key(self, key):
|
|
585
|
-
return json.dumps(key, sort_keys=True, separators=(",", ":")).encode("UTF-8")
|
|
586
|
-
|
|
587
|
-
def __setitem__(self, key, val):
|
|
588
|
-
self._cache.__setitem__(self._key(key), self._key(val))
|
|
589
|
-
|
|
590
|
-
def __getitem__(self, key):
|
|
591
|
-
return json.loads(self._cache.__getitem__(self._key(key)).decode("UTF-8"))
|
|
592
|
-
|
|
593
|
-
|
|
594
654
|
#
|
|
595
655
|
# MEMCACHED
|
|
596
656
|
#
|
|
597
|
-
# FIXME what about bytes?
|
|
598
657
|
class JsonSerde:
|
|
599
658
|
"""JSON serialize/deserialize class for MemCached (``pymemcache``).
|
|
600
659
|
|
|
@@ -607,23 +666,23 @@ class JsonSerde:
|
|
|
607
666
|
|
|
608
667
|
# keep strings, else json
|
|
609
668
|
def serialize(self, key, value):
|
|
610
|
-
if isinstance(value,
|
|
611
|
-
return value
|
|
612
|
-
elif isinstance(value,
|
|
613
|
-
return value,
|
|
669
|
+
if isinstance(value, bytes):
|
|
670
|
+
return value, 1
|
|
671
|
+
elif isinstance(value, str):
|
|
672
|
+
return value.encode("utf-8"), 2
|
|
614
673
|
else:
|
|
615
|
-
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"),
|
|
674
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 3
|
|
616
675
|
|
|
617
676
|
# reverse previous serialization
|
|
618
677
|
def deserialize(self, key, value, flag):
|
|
619
678
|
if flag == 1:
|
|
620
|
-
return value
|
|
679
|
+
return value
|
|
621
680
|
elif flag == 2:
|
|
622
|
-
return
|
|
681
|
+
return value.decode("utf-8")
|
|
623
682
|
elif flag == 3:
|
|
624
|
-
return value
|
|
683
|
+
return json.loads(value.decode("utf-8"))
|
|
625
684
|
else:
|
|
626
|
-
raise Exception("Unknown serialization format")
|
|
685
|
+
raise Exception(f"Unknown serialization format: {flag}")
|
|
627
686
|
|
|
628
687
|
|
|
629
688
|
class MemCached(_KeyMutMapMix, MutableMapping):
|
|
@@ -650,8 +709,6 @@ class MemCached(_KeyMutMapMix, MutableMapping):
|
|
|
650
709
|
# short (250 bytes) ASCII without control chars nor spaces
|
|
651
710
|
# we do not use hashing which might be costly or induce collisions
|
|
652
711
|
def _key(self, key):
|
|
653
|
-
import base64
|
|
654
|
-
|
|
655
712
|
return base64.b85encode(str(key).encode("utf-8"))
|
|
656
713
|
|
|
657
714
|
def __len__(self):
|
|
@@ -690,8 +747,6 @@ class PrefixedMemCached(MemCached):
|
|
|
690
747
|
self._prefix = bytes(prefix, "utf-8")
|
|
691
748
|
|
|
692
749
|
def _key(self, key):
|
|
693
|
-
import base64
|
|
694
|
-
|
|
695
750
|
return self._prefix + base64.b85encode(str(key).encode("utf-8"))
|
|
696
751
|
|
|
697
752
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: CacheToolsUtils
|
|
3
|
-
Version: 10.
|
|
3
|
+
Version: 10.2
|
|
4
4
|
Summary: Cachetools Utilities
|
|
5
5
|
Author-email: Fabien Coelho <cachetools.utils@coelho.net>
|
|
6
6
|
License: CC0
|
|
@@ -89,6 +89,7 @@ with `cachetools`.
|
|
|
89
89
|
- `DebugCache` to trace cache calls using `logging`.
|
|
90
90
|
- `EncryptedCache` a cache with key hashing and value encryption.
|
|
91
91
|
- `ToBytesCache` map keys and values to bytes.
|
|
92
|
+
- `BytesCache` map bytes keys and values to strings.
|
|
92
93
|
|
|
93
94
|
### Cache utilities
|
|
94
95
|
|
|
@@ -43,6 +43,7 @@ with `cachetools`.
|
|
|
43
43
|
- `DebugCache` to trace cache calls using `logging`.
|
|
44
44
|
- `EncryptedCache` a cache with key hashing and value encryption.
|
|
45
45
|
- `ToBytesCache` map keys and values to bytes.
|
|
46
|
+
- `BytesCache` map bytes keys and values to strings.
|
|
46
47
|
|
|
47
48
|
### Cache utilities
|
|
48
49
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|