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 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
+ 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
- from base64 import b64decode, b64encode
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
- 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
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
- output_path.write_bytes(plaintext)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.4.17
3
+ Version: 0.4.18
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: aiohttp-socks~=0.10.1
6
6
  Requires-Dist: aiohttp~=3.12.2
@@ -1,4 +1,4 @@
1
- mm_std/__init__.py,sha256=OpZAZw3BG3wyDnlRR3p2BZO2cZcE-imScPCf2qRWuUk,3189
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=x8LTRG6-jXLucMcsWS746V7B5fGrJ1c27d08rNK5-Ng,8173
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.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,,
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,,