Skip to content

Commit c30feb7

Browse files
authored
bumped version v3.2.3 (#93)
* Updated docs, added some scripts
1 parent 9fc6930 commit c30feb7

File tree

7 files changed

+101
-40
lines changed

7 files changed

+101
-40
lines changed

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ RedisDict is a Python library that offers a convenient and familiar interface fo
1010

1111
The library includes utility functions for more complex use cases such as caching, batching, and more. By leveraging Redis for efficient key-value storage, RedisDict enables high-performance data management, maintaining efficiency even with large datasets and Redis instances.
1212

13+
14+
[Usage](#Usage) | [Types](#Types) | [Expiration](#Expiration) | [Batching](#Batching) | [Custom Types](#Custom-Types) | [Security](#Security)
15+
16+
---
17+
1318
## Features
1419

1520
* Dictionary-like interface: Use familiar Python dictionary syntax to interact with Redis.
@@ -22,7 +27,7 @@ The library includes utility functions for more complex use cases such as cachin
2227
* Custom data: types: Add custom types encoding/decoding to store your data types.
2328
* Encryption: allows for storing data encrypted, while retaining the simple dictionary interface.
2429

25-
## Example
30+
## Usage
2631

2732
```bash
2833
pip install redis-dict
@@ -277,7 +282,7 @@ print(dic)
277282
```
278283

279284
### Additional Examples
280-
For more advanced examples of RedisDict, please refer to the unit-test files in the repository. All features and functionalities are thoroughly tested in [unit tests (here)](https://github.com/Attumm/redis-dict/blob/main/tests/unit/tests.py#L1) Or take a look at load test for batching [load test](https://github.com/Attumm/redis-dict/blob/main/tests/load/load_test.py#L1).
285+
For more advanced examples of RedisDict, please refer to the unit-test files in the repository. All features and functionalities are thoroughly tested in [unit tests (here)](https://github.com/Attumm/redis-dict/blob/main/tests/unit/tests.py#L1) Or take a look at load test for batching [load test](https://github.com/Attumm/redis-dict/blob/main/tests/load/tests_load.py#L1).
281286
The unit-tests can be as used as a starting point.
282287

283288
### Nested types
@@ -302,6 +307,7 @@ encoded = json.dumps(data, cls=RedisDictJSONEncoder)
302307
result = json.loads(encoded, cls=RedisDictJSONDecoder)
303308
```
304309

310+
## Custom Types
305311
### Extending RedisDict with Custom Types
306312

307313
RedisDict supports custom type serialization. Here's how to add a new type:
@@ -351,14 +357,22 @@ dic["3"] = "three"
351357
assert list(dic.keys()) == ["1", "2", "3"]
352358
```
353359

354-
For more information on [extending types](https://github.com/Attumm/redis-dict/blob/main/tests/unit/extend_types_tests.py).
355-
### Redis Encryption
360+
For more information on [extending types](https://github.com/Attumm/redis-dict/blob/main/tests/unit/tests_extend_types.py).
361+
362+
## Security
363+
364+
Security is an important aspect of production projects. Redis-dict was developed within a strict compliance environment.
365+
Best practice in Redis is to use passwords and network encryption through TLS. However, Redis-dict offers an additional feature by using extended types.
366+
It is possible to store the values of keys encrypted. The values are encrypted with AES GCM, which is currently considered best practice for security.
367+
368+
### Storage Encryption
369+
For storing data values encrypted can be achieved using encrypted values, more documentation on that later.
370+
For now code example within this test file. [encrypted test](https://github.com/Attumm/redis-dict/blob/main/tests/unit/tests_encrypt.py).
371+
372+
### Encryption Network
356373
Setup guide for configuring and utilizing encrypted Redis TLS for redis-dict.
357374
[Setup guide](https://github.com/Attumm/redis-dict/blob/main/docs/tutorials/encrypted_redis.MD)
358375

359-
### Redis Storage Encryption
360-
For storing encrypted data values, it's possible to use extended types. Take a look at this [encrypted test](https://github.com/Attumm/redis-dict/blob/main/tests/unit/encrypt_tests.py).
361-
362376
### Tests
363377
The RedisDict library includes a comprehensive suite of tests that ensure its correctness and resilience. The test suite covers various data types, edge cases, and error handling scenarios. It also employs the Hypothesis library for property-based testing, which provides fuzz testing to evaluate the implementation
364378

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "redis-dict"
7-
version = "3.2.2"
7+
version = "3.2.3"
88
description = "Dictionary with Redis as storage backend"
99
authors = [
1010
{name = "Melvin Bijman", email = "[email protected]"},

scripts/serve_docs.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
open docs/build/html/index.html # On macOS
2+
#xdg-open docs/build/html/index.html # On Linux

scripts/start_redis_docker.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docker run --name my-redis -p 6379:6379 -d redis

tests/unit/tests_encrypt.py

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,58 +12,109 @@
1212
class EncryptedStringClassBased(str):
1313
"""A class that behaves like a string but enables encrypted storage in Redis dictionaries.
1414
15-
This class inherits from the built-in str class, providing all standard string
15+
This class inherits from the built-in `str` class, providing all standard string
1616
functionality. However, when stored in a RedisDict, it is automatically
1717
encrypted. This allows for transparent encryption of sensitive data in Redis
1818
without changing how the string is used in your Python code.
1919
2020
The class uses AES encryption with Galois/Counter Mode (GCM) for secure
21-
encryption and decryption. It is designed for testing purposes
22-
the use of proper encryption techniques, including initialization vectors (IV)
23-
and nonces.
21+
encryption and decryption.
2422
2523
Attributes:
26-
nonce (bytes): A class-level attribute representing the nonce used for encryption.
2724
iv (bytes): The initialization vector, retrieved from an environment variable.
2825
key (bytes): The encryption key, retrieved from an environment variable.
29-
30-
Note:
31-
While this class uses actual encryption for testing, in real-world applications,
32-
more robust key management and security practices should be implemented. Never
33-
use hardcoded nonces or store encryption keys in environment variables without
34-
proper security measures in production environments.
3526
"""
36-
nonce = b"0123456789abcdef"
3727

3828
def __init__(self, value: str):
29+
"""Initializes an EncryptedStringClassBased object.
30+
31+
Retrieves the initialization vector (IV) and encryption key from
32+
environment variables 'ENCRYPTION_IV' and 'ENCRYPTION_KEY' respectively,
33+
decoding them from Base64.
34+
35+
Args:
36+
value (str): The string value to be encapsulated and potentially encrypted.
37+
"""
3938
self.value = value
4039
self.iv = base64.b64decode(os.environ['ENCRYPTION_IV'])
4140
self.key = base64.b64decode(os.environ['ENCRYPTION_KEY'])
4241

4342
def __str__(self):
43+
"""Returns the decrypted string value.
44+
45+
This method is called when the object is used in a string context,
46+
returning the original string value.
47+
48+
Returns:
49+
str: The original string value.
50+
"""
4451
return self.value
4552

4653
def __repr__(self):
54+
"""Returns a string representation of the EncryptedStringClassBased object.
55+
56+
This method provides a string that, when evaluated, would recreate the object.
57+
58+
Returns:
59+
str: A string representation of the object, in the format
60+
"EncryptedStringClassBased('value')".
61+
"""
4762
return f"EncryptedStringClassBased('{self.value}')"
4863

4964
def encode(self) -> str:
50-
cipher = Cipher(algorithms.AES(self.key), modes.GCM(self.nonce), backend=default_backend())
65+
"""Encrypts the string value using AES-GCM.
66+
67+
Retrieves the encryption key and initialization vector (IV) from instance attributes
68+
(which are loaded from environment variables during initialization).
69+
Generates a random nonce for each encryption operation.
70+
Uses AES in GCM mode to encrypt the string value.
71+
Combines the nonce, IV, GCM tag, and ciphertext, then Base64 encodes the result.
72+
73+
Returns:
74+
str: Base64 encoded string containing the nonce, IV, GCM tag, and ciphertext.
75+
This encoded string represents the encrypted value.
76+
"""
77+
key = self.key or base64.b64decode(os.environ['ENCRYPTION_KEY'])
78+
iv = self.iv or base64.b64decode(os.environ['ENCRYPTION_IV'])
79+
80+
nonce = os.urandom(16)
81+
82+
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce), backend=default_backend())
5183
encryptor = cipher.encryptor()
5284

5385
encrypted_data = encryptor.update(self.value.encode('utf-8', errors='surrogatepass')) + encryptor.finalize()
54-
return str(base64.b64encode(self.iv + self.nonce + encryptor.tag + encrypted_data).decode('utf-8'))
86+
87+
combined_data = nonce + iv + encryptor.tag + encrypted_data
88+
return str(base64.b64encode(combined_data).decode('utf-8'))
5589

5690
@classmethod
5791
def decode(cls, encrypted_value: str) -> 'EncryptedStringClassBased':
58-
iv = base64.b64decode(os.environ['ENCRYPTION_IV'])
92+
"""Decrypts an encrypted string value.
93+
94+
Decodes the Base64 encoded encrypted string.
95+
Extracts the nonce, IV, GCM tag, and ciphertext from the decoded bytes.
96+
Retrieves the encryption key and initialization vector (IV) from environment variables.
97+
Uses AES in GCM mode with the extracted nonce and tag to decrypt the ciphertext.
98+
99+
Args:
100+
encrypted_value (str): Base64 encoded string representing the encrypted value.
101+
102+
Returns:
103+
EncryptedStringClassBased: A new `EncryptedStringClassBased` object containing
104+
the decrypted string value.
105+
"""
59106
key = base64.b64decode(os.environ['ENCRYPTION_KEY'])
60-
nonce = cls.nonce
107+
iv = base64.b64decode(os.environ['ENCRYPTION_IV'])
108+
109+
encrypted_data_bytes = base64.b64decode(encrypted_value)
61110

62-
encrypted_data = base64.b64decode(encrypted_value)
63-
tag = encrypted_data[len(iv) + len(nonce):len(iv) + len(nonce) + 16]
64-
ciphertext = encrypted_data[len(iv) + len(nonce) + 16:]
111+
nonce_from_storage = encrypted_data_bytes[:16]
112+
iv_from_storage = encrypted_data_bytes[16:16 + len(iv)]
113+
tag = encrypted_data_bytes[16 + len(iv):16 + len(iv) + 16]
114+
ciphertext = encrypted_data_bytes[16 + len(iv) + 16:]
65115

66-
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce, tag), backend=default_backend())
116+
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce_from_storage, tag),
117+
backend=default_backend())
67118
decryptor = cipher.decryptor()
68119

69120
decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
@@ -108,13 +159,13 @@ def test_encrypted_string_encoding_and_decoding(self):
108159
iv = base64.b64decode(os.environ['ENCRYPTION_IV'])
109160
key = base64.b64decode(os.environ['ENCRYPTION_KEY'])
110161

111-
encode_encrypted_function = encode_encrypted_string(iv, key, EncryptedStringClassBased.nonce)
162+
#encode_encrypted_function = encode_encrypted_string(iv, key, EncryptedStringClassBased.nonce)
112163

113164
redis_dict.extends_type(EncryptedStringClassBased)
114165
key = "foo"
115166
expected_type = EncryptedStringClassBased.__name__
116167
expected = "foobar"
117-
encoded_expected = encode_encrypted_function(expected)
168+
#encoded_expected = encode_encrypted_function(expected)
118169

119170
redis_dict[key] = EncryptedStringClassBased(expected)
120171

@@ -123,11 +174,11 @@ def test_encrypted_string_encoding_and_decoding(self):
123174
self.assertNotEqual(internal_result_value, expected)
124175

125176
self.assertEqual(internal_result_type, expected_type)
126-
self.assertEqual(internal_result_value, encoded_expected)
177+
#self.assertEqual(internal_result_value, encoded_expected)
127178

128179
result = redis_dict[key]
129180

130-
self.assertNotEqual(encoded_expected, expected)
181+
#self.assertNotEqual(encoded_expected, expected)
131182
self.assertIsInstance(result, EncryptedStringClassBased)
132183
self.assertEqual(result, expected)
133184

tests/unit/tests_extend_types.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def helper_get_redis_internal_value(self, key):
5757
internal_result_type, internal_result_value = stored_in_redis_as.split(sep, 1)
5858
return internal_result_type, internal_result_value
5959

60+
6061
class TestRedisDictExtendTypesDefault(BaseRedisDictTest):
6162

6263
def test_customer_encoding_and_decoding(self):
@@ -168,9 +169,6 @@ def test_person_encoding_decoding_should_remain_equal(self):
168169
self.assertEqual(result_two.address, expected_person.address)
169170

170171

171-
# Start test for simple encryption storing of data.
172-
173-
174172
class EncryptedRot13String(str):
175173
"""
176174
A class that behaves exactly like a string but has a distinct type for redis-dict encoding and decoding.
@@ -557,7 +555,6 @@ def test_compression_size_reduction(self):
557555
self.assertEqual(decoded, expected)
558556
self.assertEqual(len(decoded), original_size)
559557

560-
561558
def test_compression_timing_comparison(self):
562559
"""Compare timing of operations between compressed and uncompressed strings"""
563560
redis_dict = self.redis_dict # A new instance for regular strings
@@ -596,7 +593,6 @@ def test_compression_timing_comparison(self):
596593
print(f"Regular string get time: {regular_get_time:.6f} seconds")
597594

598595

599-
600596
class TestNewTypeComplianceFailures(BaseRedisDictTest):
601597
def test_missing_encode_method(self):
602598
class MissingEncodeMethod:

tests/unit/tests_standard_types.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import sys
22
import unittest
33

4-
from pathlib import Path
54
from uuid import UUID
65
from pathlib import Path
76
from decimal import Decimal
@@ -10,8 +9,6 @@
109

1110
from redis_dict import RedisDict
1211

13-
import src.redis_dict.type_management
14-
1512
sys.path.append(str(Path(__file__).parent.parent.parent / "src"))
1613
from redis_dict.type_management import _default_decoder
1714

0 commit comments

Comments
 (0)