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 +4 -3
- mm_std/crypto/__init__.py +0 -0
- mm_std/crypto/openssl.py +109 -0
- {mm_std-0.4.16.dist-info → mm_std-0.4.18.dist-info}/METADATA +4 -4
- {mm_std-0.4.16.dist-info → mm_std-0.4.18.dist-info}/RECORD +7 -5
- /mm_std/{crypto.py → crypto/fernet.py} +0 -0
- {mm_std-0.4.16.dist-info → mm_std-0.4.18.dist-info}/WHEEL +0 -0
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
|
mm_std/crypto/openssl.py
ADDED
@@ -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.
|
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.
|
7
|
-
Requires-Dist: cryptography~=45.0.
|
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.
|
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=
|
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.
|
32
|
-
mm_std-0.4.
|
33
|
-
mm_std-0.4.
|
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
|
File without changes
|