mm-std 0.4.16__py3-none-any.whl → 0.4.17__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 +7 -3
- mm_std/crypto/__init__.py +0 -0
- mm_std/crypto/openssl.py +207 -0
- {mm_std-0.4.16.dist-info → mm_std-0.4.17.dist-info}/METADATA +4 -4
- {mm_std-0.4.16.dist-info → mm_std-0.4.17.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.17.dist-info}/WHEEL +0 -0
mm_std/__init__.py
CHANGED
@@ -10,9 +10,13 @@ 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 openssl_decrypt as openssl_decrypt
|
17
|
+
from .crypto.openssl import openssl_decrypt_base64 as openssl_decrypt_base64
|
18
|
+
from .crypto.openssl import openssl_encrypt as openssl_encrypt
|
19
|
+
from .crypto.openssl import openssl_encrypt_base64 as openssl_encrypt_base64
|
16
20
|
from .date import parse_date as parse_date
|
17
21
|
from .date import utc_delta as utc_delta
|
18
22
|
from .date import utc_now as utc_now
|
File without changes
|
mm_std/crypto/openssl.py
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
from base64 import b64decode, b64encode
|
2
|
+
from hashlib import pbkdf2_hmac
|
3
|
+
from os import urandom
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
from cryptography.hazmat.primitives import padding
|
7
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
8
|
+
|
9
|
+
MAGIC = b"Salted__"
|
10
|
+
SALT_SIZE = 8
|
11
|
+
KEY_SIZE = 32 # for AES-256
|
12
|
+
IV_SIZE = 16
|
13
|
+
|
14
|
+
|
15
|
+
def openssl_encrypt(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
|
16
|
+
"""
|
17
|
+
Encrypt a file using OpenSSL-compatible AES-256-CBC with PBKDF2 and output in binary format.
|
18
|
+
|
19
|
+
This function creates encrypted files that are fully compatible with OpenSSL command:
|
20
|
+
openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -in message.txt -out message.enc
|
21
|
+
|
22
|
+
Args:
|
23
|
+
input_path: Path to the input file to encrypt
|
24
|
+
output_path: Path where the encrypted binary file will be saved
|
25
|
+
password: Password for encryption
|
26
|
+
iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
|
27
|
+
|
28
|
+
Raises:
|
29
|
+
ValueError: If iterations < 1000
|
30
|
+
|
31
|
+
Example:
|
32
|
+
>>> from pathlib import Path
|
33
|
+
>>> openssl_encrypt(Path("secret.txt"), Path("secret.enc"), "mypassword")
|
34
|
+
|
35
|
+
# Decrypt with OpenSSL:
|
36
|
+
# openssl enc -d -aes-256-cbc -pbkdf2 -iter 1000000 -in secret.enc -out secret_decrypted.txt -pass pass:mypassword
|
37
|
+
"""
|
38
|
+
if iterations < 1000:
|
39
|
+
raise ValueError("Iteration count must be at least 1000 for security")
|
40
|
+
|
41
|
+
data: bytes = input_path.read_bytes()
|
42
|
+
salt: bytes = urandom(SALT_SIZE)
|
43
|
+
|
44
|
+
key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
|
45
|
+
key: bytes = key_iv[:KEY_SIZE]
|
46
|
+
iv: bytes = key_iv[KEY_SIZE:]
|
47
|
+
|
48
|
+
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
49
|
+
padded_data: bytes = padder.update(data) + padder.finalize()
|
50
|
+
|
51
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
52
|
+
encryptor = cipher.encryptor()
|
53
|
+
ciphertext: bytes = encryptor.update(padded_data) + encryptor.finalize()
|
54
|
+
|
55
|
+
output_path.write_bytes(MAGIC + salt + ciphertext)
|
56
|
+
|
57
|
+
|
58
|
+
def openssl_decrypt(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
|
59
|
+
"""
|
60
|
+
Decrypt a binary file created by OpenSSL-compatible AES-256-CBC with PBKDF2.
|
61
|
+
|
62
|
+
This function decrypts files that were encrypted with OpenSSL command:
|
63
|
+
openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -in message.txt -out message.enc
|
64
|
+
|
65
|
+
Args:
|
66
|
+
input_path: Path to the encrypted binary file
|
67
|
+
output_path: Path where the decrypted file will be saved
|
68
|
+
password: Password for decryption
|
69
|
+
iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
|
70
|
+
|
71
|
+
Raises:
|
72
|
+
ValueError: If iterations < 1000, invalid file format, or wrong password
|
73
|
+
|
74
|
+
Example:
|
75
|
+
>>> from pathlib import Path
|
76
|
+
>>> openssl_decrypt(Path("secret.enc"), Path("secret_decrypted.txt"), "mypassword")
|
77
|
+
|
78
|
+
# Encrypt with OpenSSL:
|
79
|
+
# openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -in secret.txt -out secret.enc -pass pass:mypassword
|
80
|
+
"""
|
81
|
+
if iterations < 1000:
|
82
|
+
raise ValueError("Iteration count must be at least 1000 for security")
|
83
|
+
|
84
|
+
raw: bytes = input_path.read_bytes()
|
85
|
+
if not raw.startswith(MAGIC):
|
86
|
+
raise ValueError("Invalid file format: missing OpenSSL Salted header")
|
87
|
+
|
88
|
+
salt: bytes = raw[8:16]
|
89
|
+
ciphertext: bytes = raw[16:]
|
90
|
+
|
91
|
+
key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
|
92
|
+
key: bytes = key_iv[:KEY_SIZE]
|
93
|
+
iv: bytes = key_iv[KEY_SIZE:]
|
94
|
+
|
95
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
96
|
+
decryptor = cipher.decryptor()
|
97
|
+
padded_plaintext: bytes = decryptor.update(ciphertext) + decryptor.finalize()
|
98
|
+
|
99
|
+
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
100
|
+
try:
|
101
|
+
plaintext: bytes = unpadder.update(padded_plaintext) + unpadder.finalize()
|
102
|
+
except ValueError as e:
|
103
|
+
raise ValueError("Decryption failed: invalid padding or wrong password") from e
|
104
|
+
|
105
|
+
output_path.write_bytes(plaintext)
|
106
|
+
|
107
|
+
|
108
|
+
def openssl_encrypt_base64(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
|
109
|
+
"""
|
110
|
+
Encrypt a file using OpenSSL-compatible AES-256-CBC with PBKDF2 and output in base64 format.
|
111
|
+
|
112
|
+
This function creates encrypted files that are fully compatible with OpenSSL command:
|
113
|
+
openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -base64 -in message.txt -out message.enc
|
114
|
+
|
115
|
+
Args:
|
116
|
+
input_path: Path to the input file to encrypt
|
117
|
+
output_path: Path where the encrypted base64 file will be saved
|
118
|
+
password: Password for encryption
|
119
|
+
iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
|
120
|
+
|
121
|
+
Raises:
|
122
|
+
ValueError: If iterations < 1000
|
123
|
+
|
124
|
+
Example:
|
125
|
+
>>> from pathlib import Path
|
126
|
+
>>> openssl_encrypt_base64(Path("secret.txt"), Path("secret.enc"), "mypassword")
|
127
|
+
|
128
|
+
# Decrypt with OpenSSL:
|
129
|
+
# openssl enc -d -aes-256-cbc -pbkdf2 -iter 1000000 -base64 -in secret.enc -out secret_decrypted.txt -pass pass:mypassword
|
130
|
+
"""
|
131
|
+
if iterations < 1000:
|
132
|
+
raise ValueError("Iteration count must be at least 1000 for security")
|
133
|
+
|
134
|
+
data: bytes = input_path.read_bytes()
|
135
|
+
salt: bytes = urandom(SALT_SIZE)
|
136
|
+
|
137
|
+
key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
|
138
|
+
key: bytes = key_iv[:KEY_SIZE]
|
139
|
+
iv: bytes = key_iv[KEY_SIZE:]
|
140
|
+
|
141
|
+
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
142
|
+
padded_data: bytes = padder.update(data) + padder.finalize()
|
143
|
+
|
144
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
145
|
+
encryptor = cipher.encryptor()
|
146
|
+
ciphertext: bytes = encryptor.update(padded_data) + encryptor.finalize()
|
147
|
+
|
148
|
+
# Encode binary data to base64
|
149
|
+
binary_data: bytes = MAGIC + salt + ciphertext
|
150
|
+
base64_data: str = b64encode(binary_data).decode("ascii")
|
151
|
+
output_path.write_text(base64_data)
|
152
|
+
|
153
|
+
|
154
|
+
def openssl_decrypt_base64(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
|
155
|
+
"""
|
156
|
+
Decrypt a base64-encoded file created by OpenSSL-compatible AES-256-CBC with PBKDF2.
|
157
|
+
|
158
|
+
This function decrypts files that were encrypted with OpenSSL command:
|
159
|
+
openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -base64 -in message.txt -out message.enc
|
160
|
+
|
161
|
+
Args:
|
162
|
+
input_path: Path to the encrypted base64 file
|
163
|
+
output_path: Path where the decrypted file will be saved
|
164
|
+
password: Password for decryption
|
165
|
+
iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
|
166
|
+
|
167
|
+
Raises:
|
168
|
+
ValueError: If iterations < 1000, invalid file format, or wrong password
|
169
|
+
|
170
|
+
Example:
|
171
|
+
>>> from pathlib import Path
|
172
|
+
>>> openssl_decrypt_base64(Path("secret.enc"), Path("secret_decrypted.txt"), "mypassword")
|
173
|
+
|
174
|
+
# Encrypt with OpenSSL:
|
175
|
+
# openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -base64 -in secret.txt -out secret.enc -pass pass:mypassword
|
176
|
+
"""
|
177
|
+
if iterations < 1000:
|
178
|
+
raise ValueError("Iteration count must be at least 1000 for security")
|
179
|
+
|
180
|
+
# Decode base64 to binary data
|
181
|
+
try:
|
182
|
+
base64_data: str = input_path.read_text().strip()
|
183
|
+
raw: bytes = b64decode(base64_data)
|
184
|
+
except Exception as e:
|
185
|
+
raise ValueError("Invalid base64 format") from e
|
186
|
+
|
187
|
+
if not raw.startswith(MAGIC):
|
188
|
+
raise ValueError("Invalid file format: missing OpenSSL Salted header")
|
189
|
+
|
190
|
+
salt: bytes = raw[8:16]
|
191
|
+
ciphertext: bytes = raw[16:]
|
192
|
+
|
193
|
+
key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
|
194
|
+
key: bytes = key_iv[:KEY_SIZE]
|
195
|
+
iv: bytes = key_iv[KEY_SIZE:]
|
196
|
+
|
197
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
198
|
+
decryptor = cipher.decryptor()
|
199
|
+
padded_plaintext: bytes = decryptor.update(ciphertext) + decryptor.finalize()
|
200
|
+
|
201
|
+
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
202
|
+
try:
|
203
|
+
plaintext: bytes = unpadder.update(padded_plaintext) + unpadder.finalize()
|
204
|
+
except ValueError as e:
|
205
|
+
raise ValueError("Decryption failed: invalid padding or wrong password") from e
|
206
|
+
|
207
|
+
output_path.write_bytes(plaintext)
|
@@ -1,12 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mm-std
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.17
|
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=OpZAZw3BG3wyDnlRR3p2BZO2cZcE-imScPCf2qRWuUk,3189
|
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=x8LTRG6-jXLucMcsWS746V7B5fGrJ1c27d08rNK5-Ng,8173
|
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.17.dist-info/METADATA,sha256=0rG7NElzy9ZOLdx3hnag32xev6avidgXTZe46h85waU,414
|
34
|
+
mm_std-0.4.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
35
|
+
mm_std-0.4.17.dist-info/RECORD,,
|
File without changes
|
File without changes
|