mm-std 0.4.16__py3-none-any.whl → 0.4.18__py3-none-any.whl

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.
mm_std/__init__.py CHANGED
@@ -10,9 +10,10 @@ from .concurrency.sync_decorators import synchronized_parameter as synchronized_
10
10
  from .concurrency.sync_scheduler import Scheduler as Scheduler
11
11
  from .concurrency.sync_task_runner import ConcurrentTasks as ConcurrentTasks
12
12
  from .config import BaseConfig as BaseConfig
13
- from .crypto import fernet_decrypt as fernet_decrypt
14
- from .crypto import fernet_encrypt as fernet_encrypt
15
- from .crypto import fernet_generate_key as fernet_generate_key
13
+ from .crypto.fernet import fernet_decrypt as fernet_decrypt
14
+ from .crypto.fernet import fernet_encrypt as fernet_encrypt
15
+ from .crypto.fernet import fernet_generate_key as fernet_generate_key
16
+ from .crypto.openssl import OpensslAes256Cbc as OpensslAes256Cbc
16
17
  from .date import parse_date as parse_date
17
18
  from .date import utc_delta as utc_delta
18
19
  from .date import utc_now as utc_now
File without changes
@@ -0,0 +1,109 @@
1
+ import base64
2
+ import secrets
3
+ from hashlib import pbkdf2_hmac
4
+
5
+ from cryptography.hazmat.primitives import padding
6
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
7
+
8
+
9
+ class OpensslAes256Cbc:
10
+ """
11
+ AES-256-CBC encryption/decryption compatible with OpenSSL's `enc -aes-256-cbc -pbkdf2 -iter 1000000`.
12
+
13
+ Provides both raw-byte and Base64-encoded interfaces:
14
+ • encrypt_bytes / decrypt_bytes: work with bytes
15
+ • encrypt_base64 / decrypt_base64: work with Base64 strings
16
+
17
+ Usage:
18
+ >>> cipher = OpensslAes256Cbc(password="mypassword")
19
+ >>> # raw bytes
20
+ >>> ciphertext = cipher.encrypt_bytes(b"secret")
21
+ >>> plaintext = cipher.decrypt_bytes(ciphertext)
22
+ >>> # Base64 convenience
23
+ >>> token = cipher.encrypt_base64("secret message")
24
+ >>> result = cipher.decrypt_base64(token)
25
+ >>> print(result)
26
+ secret message
27
+
28
+ OpenSSL compatibility:
29
+ echo "secret message" |
30
+ openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -base64 -pass pass:mypassword
31
+
32
+ echo "U2FsdGVkX1/dGGdg6SExWgtKxvuLroWqhezy54aTt1g=" |
33
+ openssl enc -d -aes-256-cbc -pbkdf2 -iter 1000000 -base64 -pass pass:mypassword
34
+ """
35
+
36
+ MAGIC_HEADER = b"Salted__"
37
+ SALT_SIZE = 8
38
+ KEY_SIZE = 32 # AES-256
39
+ IV_SIZE = 16 # AES block size
40
+ ITERATIONS = 1_000_000
41
+ HEADER_LEN = len(MAGIC_HEADER)
42
+
43
+ def __init__(self, password: str) -> None:
44
+ """
45
+ Initialize the cipher with password. Uses a fixed iteration count of 1,000,000.
46
+
47
+ Args:
48
+ password: Password for encryption/decryption
49
+ """
50
+ self._password = password.encode("utf-8")
51
+
52
+ def _derive_key_iv(self, salt: bytes) -> tuple[bytes, bytes]:
53
+ key_iv = pbkdf2_hmac(
54
+ hash_name="sha256", password=self._password, salt=salt, iterations=self.ITERATIONS, dklen=self.KEY_SIZE + self.IV_SIZE
55
+ )
56
+ return key_iv[: self.KEY_SIZE], key_iv[self.KEY_SIZE :]
57
+
58
+ def encrypt_bytes(self, plaintext: bytes) -> bytes:
59
+ """Encrypt raw bytes and return encrypted bytes (OpenSSL compatible)."""
60
+ salt = secrets.token_bytes(self.SALT_SIZE)
61
+ key, iv = self._derive_key_iv(salt)
62
+
63
+ padder = padding.PKCS7(algorithms.AES.block_size).padder()
64
+ padded = padder.update(plaintext) + padder.finalize()
65
+
66
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
67
+ encryptor = cipher.encryptor()
68
+ ciphertext = encryptor.update(padded) + encryptor.finalize()
69
+
70
+ return self.MAGIC_HEADER + salt + ciphertext
71
+
72
+ def decrypt_bytes(self, encrypted: bytes) -> bytes:
73
+ """Decrypt raw encrypted bytes (as produced by encrypt_bytes)."""
74
+ if not encrypted.startswith(self.MAGIC_HEADER):
75
+ raise ValueError("Invalid format: missing OpenSSL salt header")
76
+
77
+ salt = encrypted[self.HEADER_LEN : self.HEADER_LEN + self.SALT_SIZE]
78
+ ciphertext = encrypted[self.HEADER_LEN + self.SALT_SIZE :]
79
+
80
+ key, iv = self._derive_key_iv(salt)
81
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
82
+ decryptor = cipher.decryptor()
83
+
84
+ try:
85
+ padded = decryptor.update(ciphertext) + decryptor.finalize()
86
+ except ValueError as exc:
87
+ raise ValueError("Decryption failed: wrong password or corrupted data") from exc
88
+
89
+ unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
90
+ try:
91
+ data = unpadder.update(padded) + unpadder.finalize()
92
+ except ValueError as exc:
93
+ raise ValueError("Decryption failed: wrong password or corrupted data") from exc
94
+
95
+ return data
96
+
97
+ def encrypt_base64(self, plaintext: str) -> str:
98
+ """Encrypt a UTF-8 string and return Base64-encoded encrypted data."""
99
+ raw = self.encrypt_bytes(plaintext.encode("utf-8"))
100
+ return base64.b64encode(raw).decode("ascii")
101
+
102
+ def decrypt_base64(self, b64_encoded: str) -> str:
103
+ """Decode Base64, decrypt bytes, and return UTF-8 string."""
104
+ try:
105
+ raw = base64.b64decode(b64_encoded.strip())
106
+ except Exception as exc:
107
+ raise ValueError("Invalid base64 format") from exc
108
+ plaintext_bytes = self.decrypt_bytes(raw)
109
+ return plaintext_bytes.decode("utf-8")
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.4.16
3
+ Version: 0.4.18
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: aiohttp-socks~=0.10.1
6
- Requires-Dist: aiohttp~=3.11.18
7
- Requires-Dist: cryptography~=45.0.2
6
+ Requires-Dist: aiohttp~=3.12.2
7
+ Requires-Dist: cryptography~=45.0.3
8
8
  Requires-Dist: pydantic-settings>=2.9.1
9
- Requires-Dist: pydantic~=2.11.4
9
+ Requires-Dist: pydantic~=2.11.5
10
10
  Requires-Dist: pydash~=8.0.5
11
11
  Requires-Dist: python-dotenv~=1.1.0
12
12
  Requires-Dist: requests[socks]~=2.32.3
@@ -1,7 +1,6 @@
1
- mm_std/__init__.py,sha256=atBLjXR-O2osgfiZru7mA2SSAPb60fRRDFqxf1soMZM,2888
1
+ mm_std/__init__.py,sha256=PGjf3Cs_tDvJW-Mff2NADmi1dLDePVMHIvQsD3wleH8,2974
2
2
  mm_std/command.py,sha256=ze286wjUjg0QSTgIu-2WZks53_Vclg69UaYYgPpQvCU,1283
3
3
  mm_std/config.py,sha256=3-FxFCtOffY2t9vuu9adojwbzTPwdAH2P6qDaZnHLbY,3206
4
- mm_std/crypto.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
5
4
  mm_std/date.py,sha256=976eEkSONuNqHQBgSRu8hrtH23tJqztbmHFHLdbP2TY,1879
6
5
  mm_std/dict.py,sha256=DxFbZnl5KK4vJ4wLH_pCT0PWzfaIL3YyLNpRsVexfjw,1465
7
6
  mm_std/env.py,sha256=5zaR9VeIfObN-4yfgxoFeU5IM1GDeZZj9SuYf7t9sOA,125
@@ -24,10 +23,13 @@ mm_std/concurrency/async_task_runner.py,sha256=EN7tN2enkVYVgDbhSiAr-_W4o9m9wBXCv
24
23
  mm_std/concurrency/sync_decorators.py,sha256=syCQBOmN7qPO55yzgJB2rbkh10CVww376hmyvs6e5tA,1080
25
24
  mm_std/concurrency/sync_scheduler.py,sha256=j4tBL_cBI1spr0cZplTA7N2CoYsznuORMeRN8rpR6gY,2407
26
25
  mm_std/concurrency/sync_task_runner.py,sha256=s5JPlLYLGQGHIxy4oDS-PN7O9gcy-yPZFoNm8RQwzcw,1780
26
+ mm_std/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ mm_std/crypto/fernet.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
28
+ mm_std/crypto/openssl.py,sha256=HyiYNgn5IrC3CWfe3qcLKJpAsbcSlCUoPvEg1vxbdvo,4274
27
29
  mm_std/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
30
  mm_std/http/http_request.py,sha256=6bg3t49c3dG0jKRFxhcceeYb5yKrMoZwuyb25zBG3tY,4088
29
31
  mm_std/http/http_request_sync.py,sha256=zXLeDplYWTFIwaD1Ydyg9yTi37WcI-fReLM0mVnuvhM,1835
30
32
  mm_std/http/http_response.py,sha256=7ZllZFPKJ9s6m-18Dfhrm7hwc2XFnyX7ppt0O8gNmlE,3916
31
- mm_std-0.4.16.dist-info/METADATA,sha256=_cSVs-cNmY9lZicgVuawtbNuG5vDmpn6rrFQY_bzwI4,415
32
- mm_std-0.4.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- mm_std-0.4.16.dist-info/RECORD,,
33
+ mm_std-0.4.18.dist-info/METADATA,sha256=eUTWVnKfAtzbA_v27albJ7pj4iuCTlbPpTWOTggjDQw,414
34
+ mm_std-0.4.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ mm_std-0.4.18.dist-info/RECORD,,
File without changes