mm-std 0.4.17__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
@@ -13,10 +13,7 @@ from .config import BaseConfig as BaseConfig
|
|
13
13
|
from .crypto.fernet import fernet_decrypt as fernet_decrypt
|
14
14
|
from .crypto.fernet import fernet_encrypt as fernet_encrypt
|
15
15
|
from .crypto.fernet import fernet_generate_key as fernet_generate_key
|
16
|
-
from .crypto.openssl import
|
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
|
+
from .crypto.openssl import OpensslAes256Cbc as OpensslAes256Cbc
|
20
17
|
from .date import parse_date as parse_date
|
21
18
|
from .date import utc_delta as utc_delta
|
22
19
|
from .date import utc_now as utc_now
|
mm_std/crypto/openssl.py
CHANGED
@@ -1,207 +1,109 @@
|
|
1
|
-
|
1
|
+
import base64
|
2
|
+
import secrets
|
2
3
|
from hashlib import pbkdf2_hmac
|
3
|
-
from os import urandom
|
4
|
-
from pathlib import Path
|
5
4
|
|
6
5
|
from cryptography.hazmat.primitives import padding
|
7
6
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
8
7
|
|
9
|
-
MAGIC = b"Salted__"
|
10
|
-
SALT_SIZE = 8
|
11
|
-
KEY_SIZE = 32 # for AES-256
|
12
|
-
IV_SIZE = 16
|
13
8
|
|
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:
|
9
|
+
class OpensslAes256Cbc:
|
59
10
|
"""
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
80
34
|
"""
|
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
35
|
|
207
|
-
|
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,4 +1,4 @@
|
|
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
4
|
mm_std/date.py,sha256=976eEkSONuNqHQBgSRu8hrtH23tJqztbmHFHLdbP2TY,1879
|
@@ -25,11 +25,11 @@ mm_std/concurrency/sync_scheduler.py,sha256=j4tBL_cBI1spr0cZplTA7N2CoYsznuORMeRN
|
|
25
25
|
mm_std/concurrency/sync_task_runner.py,sha256=s5JPlLYLGQGHIxy4oDS-PN7O9gcy-yPZFoNm8RQwzcw,1780
|
26
26
|
mm_std/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
27
|
mm_std/crypto/fernet.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
|
28
|
-
mm_std/crypto/openssl.py,sha256=
|
28
|
+
mm_std/crypto/openssl.py,sha256=HyiYNgn5IrC3CWfe3qcLKJpAsbcSlCUoPvEg1vxbdvo,4274
|
29
29
|
mm_std/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
30
|
mm_std/http/http_request.py,sha256=6bg3t49c3dG0jKRFxhcceeYb5yKrMoZwuyb25zBG3tY,4088
|
31
31
|
mm_std/http/http_request_sync.py,sha256=zXLeDplYWTFIwaD1Ydyg9yTi37WcI-fReLM0mVnuvhM,1835
|
32
32
|
mm_std/http/http_response.py,sha256=7ZllZFPKJ9s6m-18Dfhrm7hwc2XFnyX7ppt0O8gNmlE,3916
|
33
|
-
mm_std-0.4.
|
34
|
-
mm_std-0.4.
|
35
|
-
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
|