cryptncompress 2.0.0__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.
- cryptncompress/__init__.py +1 -0
- cryptncompress/compress.py +124 -0
- cryptncompress/crypto.py +484 -0
- cryptncompress/errors.py +43 -0
- cryptncompress-2.0.0.dist-info/METADATA +17 -0
- cryptncompress-2.0.0.dist-info/RECORD +8 -0
- cryptncompress-2.0.0.dist-info/WHEEL +5 -0
- cryptncompress-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
``compress`` module
|
|
5
|
+
=================
|
|
6
|
+
|
|
7
|
+
Module used to compress/decompress files in the lola project
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import zipfile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Extract:
|
|
16
|
+
"""
|
|
17
|
+
Create a Extract object to hold information on compressed file, where to store output, etc ...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, archive: str, output_dir: str, files_in_archive: int = None):
|
|
21
|
+
"""
|
|
22
|
+
Create a Extract object which hold information on how to extract a file
|
|
23
|
+
A verification step is used to check is the archive is a zip file
|
|
24
|
+
|
|
25
|
+
:param archive: path of the archive file
|
|
26
|
+
:type archive: str
|
|
27
|
+
:param output_dir: path of the directory when to extract files
|
|
28
|
+
:type output_dir: str
|
|
29
|
+
:param files_in_archive: If set, check the number of file in the archive
|
|
30
|
+
Raise an error if there are more or less files. Default = None
|
|
31
|
+
:type files_in_archive: int
|
|
32
|
+
:return: an extract object
|
|
33
|
+
:rtype: Extract
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
self.archive = Extract.__check_archive(archive, files_in_archive)
|
|
37
|
+
self.output_dir = Path(output_dir)
|
|
38
|
+
if not self.output_dir.is_dir():
|
|
39
|
+
raise FileNotFoundError(f"{self.output_dir} does not exists.")
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def __check_archive(archive: str, file_number: int = None) -> Path:
|
|
43
|
+
"""
|
|
44
|
+
Check if the archive is a zipfile and contains the good number of files
|
|
45
|
+
|
|
46
|
+
:param archive: path of the archive
|
|
47
|
+
:type archive: str
|
|
48
|
+
:param file_number: number of file in the archive
|
|
49
|
+
:type file_number: int
|
|
50
|
+
:raise ExtractError: if the number of file is different or
|
|
51
|
+
if the archive does not exist
|
|
52
|
+
:return: The path of the archive
|
|
53
|
+
:rtype: zipfile.ZipFile
|
|
54
|
+
"""
|
|
55
|
+
archive = Path(archive)
|
|
56
|
+
if not archive.is_file():
|
|
57
|
+
raise IOError(f"'{archive}' file does not exist")
|
|
58
|
+
|
|
59
|
+
if not zipfile.is_zipfile(archive):
|
|
60
|
+
raise IOError(f"'{archive}' file is not a zipfile")
|
|
61
|
+
|
|
62
|
+
zip_archive = zipfile.ZipFile(archive)
|
|
63
|
+
if file_number:
|
|
64
|
+
if len(zip_archive.namelist()) != file_number:
|
|
65
|
+
raise IOError(
|
|
66
|
+
f"'{archive}' archive have more or less than {file_number} file(s)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return archive
|
|
70
|
+
|
|
71
|
+
def extract(self):
|
|
72
|
+
"""
|
|
73
|
+
Extract files
|
|
74
|
+
"""
|
|
75
|
+
with zipfile.ZipFile(self.archive, "r") as zipObj:
|
|
76
|
+
zipObj.extractall(self.output_dir)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Compress:
|
|
80
|
+
"""
|
|
81
|
+
Create a Compress object to hold information on how to compress file, where to store output, etc ...
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, output_archive: str):
|
|
85
|
+
"""
|
|
86
|
+
Create a Compress object which hold information on how to compress files
|
|
87
|
+
A verification step is used to check is the files exists
|
|
88
|
+
|
|
89
|
+
:param output_archive: path of the output archive file
|
|
90
|
+
:type output_archive: str
|
|
91
|
+
:return: a Compress object
|
|
92
|
+
:rtype: Compress
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
self.output_archive = Path(output_archive)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def __check_file(f: str) -> Path:
|
|
99
|
+
"""
|
|
100
|
+
Check if the exist and is readable
|
|
101
|
+
|
|
102
|
+
:param f: path of the file to check
|
|
103
|
+
:type f: str
|
|
104
|
+
:return: A Path object of the file
|
|
105
|
+
:rtype: pathlib.Path
|
|
106
|
+
"""
|
|
107
|
+
my_file = Path(f)
|
|
108
|
+
if not my_file.is_file():
|
|
109
|
+
raise IOError(f"'{my_file}' file does not exist")
|
|
110
|
+
|
|
111
|
+
return my_file
|
|
112
|
+
|
|
113
|
+
def compress(self, files: list):
|
|
114
|
+
"""
|
|
115
|
+
Compress a list of files
|
|
116
|
+
|
|
117
|
+
:param files: a list of files
|
|
118
|
+
:type files: list
|
|
119
|
+
"""
|
|
120
|
+
sanitized_list_files = [self.__check_file(f) for f in files]
|
|
121
|
+
|
|
122
|
+
zf = zipfile.ZipFile(self.output_archive, 'w', zipfile.ZIP_DEFLATED)
|
|
123
|
+
for f in sanitized_list_files:
|
|
124
|
+
zf.write(f, f.name)
|
cryptncompress/crypto.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
``crypto`` module
|
|
5
|
+
=================
|
|
6
|
+
|
|
7
|
+
Module used to encrypt/decrypt files in the lola project in a specific format (see below)
|
|
8
|
+
|
|
9
|
+
:Example:
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
regular_file = pathlib.Path("/tmp/my_file.txt")
|
|
12
|
+
with open("<PATH_TO_PUB_KEY>", "rb") as buffer_pub_key:
|
|
13
|
+
pub_key_bytes = buffer_pub_key.read()
|
|
14
|
+
|
|
15
|
+
encryptor = Encrypt(pub_key_bytes)
|
|
16
|
+
encryptor.encrypt_file("file", regular_file, "/tmp/my_file.txt.crypt")
|
|
17
|
+
|
|
18
|
+
encrypted_file = pathlib.Path("/tmp/my_file.txt.crypt")
|
|
19
|
+
|
|
20
|
+
with open("<PATH_TO_PRIVATE_KEY>", "rb") as buffer_private_key:
|
|
21
|
+
private_key_bytes = buffer_private_key.read()
|
|
22
|
+
|
|
23
|
+
decryptor = Decrypt(private_key_bytes)
|
|
24
|
+
decryptor.decrypt_file(encrypted_file, "/tmp/my_file.txt.descrypted")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Encryption
|
|
28
|
+
----------
|
|
29
|
+
To encrypt files, you need to have 3 items and 1 optionnal:
|
|
30
|
+
- The dataset file: in json, xapi or whatever. It contains data you want to
|
|
31
|
+
transfert on the plateform
|
|
32
|
+
- The format of dataset file: If the file is xapi/json or not. This is used to
|
|
33
|
+
make a difference between dataset files to import in platforme and "file to transfert"
|
|
34
|
+
- A RSA public key (2048 bits) with x509.
|
|
35
|
+
- (Optionnal) A bordereau file: used if the dataset file is Json/xapi and need to be import in the platform
|
|
36
|
+
|
|
37
|
+
Decryption
|
|
38
|
+
----------
|
|
39
|
+
To decrypt a file, you need 2 items:
|
|
40
|
+
- An encrypted file
|
|
41
|
+
- A RSA private key (2048 bits)
|
|
42
|
+
|
|
43
|
+
File format
|
|
44
|
+
-----------
|
|
45
|
+
The format used for encryption and decryption is specific to Lola project.
|
|
46
|
+
It's a binary files with the following format:
|
|
47
|
+
- [1 Byte INTEGER]: type of the file:
|
|
48
|
+
- 1 means the file contains a xapi/json dataset, a bordereau and can be transfert to the lola platform
|
|
49
|
+
- 0 means the file is just a datafile to tranfert and can be converted with other tools later.
|
|
50
|
+
- [256 Bytes] AES symmetric key used to fast encryption/decryption of data blob. The AES key is encrypted with the RSA key
|
|
51
|
+
- [8 bytes INTEGER]: Size of the dataset in bytes into a 64bits integer
|
|
52
|
+
- [Remains] data blob. This blob could be compressed
|
|
53
|
+
|
|
54
|
+
RSA keys
|
|
55
|
+
--------
|
|
56
|
+
Couple of RSA keys can be generated with openssl with the following command
|
|
57
|
+
$ openssl req -x509 -nodes -newkey rsa:2048 -keuout private_key.pem -out public_key.pem
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
import os
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
|
|
63
|
+
from cryptncompress import errors
|
|
64
|
+
|
|
65
|
+
from cryptography import x509
|
|
66
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
67
|
+
from cryptography.hazmat.primitives import hashes
|
|
68
|
+
from cryptography.hazmat.primitives import serialization
|
|
69
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
70
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
CHUNK_SIZE = 2**25 # Easy to fit in memory
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fix_binary_data_length(binary_data: bytes):
|
|
77
|
+
"""
|
|
78
|
+
Right padding of binary data with 0 bytes
|
|
79
|
+
Fix "ValueError: The length of the provided data is not a multiple of the block length."
|
|
80
|
+
"""
|
|
81
|
+
block_length = SymmetricKey.key_size
|
|
82
|
+
binary_data_length = len(binary_data)
|
|
83
|
+
length_with_padding = (
|
|
84
|
+
binary_data_length + (block_length - binary_data_length) % block_length
|
|
85
|
+
)
|
|
86
|
+
return binary_data.ljust(length_with_padding, b"\0"), binary_data_length
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def sanitize_binary_data(binary_data: bytes, original_file_size: int = None) -> bytes:
|
|
90
|
+
"""
|
|
91
|
+
Remove all '0' added at the end of the file by the fix_binary_data_length function
|
|
92
|
+
If original file_size is given, guess the number of '0' added.
|
|
93
|
+
You should use original_file_size every time to avoid break the output file. For exemple
|
|
94
|
+
zip files add 4 '0' at the end of the file.
|
|
95
|
+
|
|
96
|
+
:param binary_data: data to sanitize
|
|
97
|
+
:type binary_data: bytes
|
|
98
|
+
:param original_file_size: size in bytes of the original file [Default: None]
|
|
99
|
+
:type original_file_size: int
|
|
100
|
+
:return: binary data after removing trailing '0'
|
|
101
|
+
:rtype: bytes
|
|
102
|
+
"""
|
|
103
|
+
if original_file_size:
|
|
104
|
+
# Use the same calculation than in fix_binary_data_length
|
|
105
|
+
length_with_padding = (
|
|
106
|
+
original_file_size + (SymmetricKey.key_size - original_file_size) % SymmetricKey.key_size
|
|
107
|
+
)
|
|
108
|
+
number_added_0 = length_with_padding - original_file_size
|
|
109
|
+
return binary_data[: len(binary_data) - number_added_0]
|
|
110
|
+
|
|
111
|
+
while binary_data[-1] == 0:
|
|
112
|
+
binary_data = binary_data[:-1]
|
|
113
|
+
return binary_data
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class FileType:
|
|
117
|
+
"""
|
|
118
|
+
Class used to convert the type of file in binary format
|
|
119
|
+
Don't use this class directly. This class is used by Encrypt and Decrypt class.
|
|
120
|
+
2 types of files availables:
|
|
121
|
+
- 'xapi' for files with xapi/json format which need to be insert directly to the platform.
|
|
122
|
+
- 'file' for files that just need to be transfered. These file are not directly manage by the platform
|
|
123
|
+
|
|
124
|
+
NOTE: In Python 3.10. Enums and pattern matching are available and can be used instead of string representation.
|
|
125
|
+
Instead of using encryptor.encrypt_file("file", input_file, output_file)
|
|
126
|
+
use this instead encryptor.encrypt_file(FileType.FILE, input_file, output_file)
|
|
127
|
+
TODO: Change the note above in python 3.10
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
supported_types = {
|
|
131
|
+
"xapi": 1,
|
|
132
|
+
"file": 0,
|
|
133
|
+
}
|
|
134
|
+
int_size = 1 # Encode filetype on only 1 byte
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def __list_supported_types_str() -> list:
|
|
138
|
+
"""
|
|
139
|
+
Return the supported types as a list of str
|
|
140
|
+
"""
|
|
141
|
+
return list(FileType.supported_types)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def str_to_bin(file_type: str) -> bytes:
|
|
145
|
+
"""
|
|
146
|
+
Convert the string representation of a filetype ("xapi" or "file") into
|
|
147
|
+
its binary representation
|
|
148
|
+
|
|
149
|
+
:param file_type: String representation of the file type
|
|
150
|
+
:type file_type: str
|
|
151
|
+
:return: Binary representation of the file type
|
|
152
|
+
:rtype: bytes
|
|
153
|
+
"""
|
|
154
|
+
supported_list = FileType.__list_supported_types_str()
|
|
155
|
+
if file_type not in supported_list:
|
|
156
|
+
raise errors.CryptoIncorrectFileType(
|
|
157
|
+
f"Unsupported File Type. Supported types : {' ,'.join(supported_list)}. Given '{file_type}'"
|
|
158
|
+
)
|
|
159
|
+
# Convert the integer of the file type to a binary with padding on 1 Byte
|
|
160
|
+
int_file_type = FileType.supported_types[file_type]
|
|
161
|
+
binary_integer = DataFile.int_to_bin(int_file_type, 1)
|
|
162
|
+
return binary_integer
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def bin_to_str(bin_str: bytes) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Convert the binary representation of a filetype (0 or 1) into
|
|
168
|
+
its string representation
|
|
169
|
+
|
|
170
|
+
:param bin_str: Binary representation of the file type
|
|
171
|
+
:type file_type: bytes
|
|
172
|
+
:return: String representation of the file type
|
|
173
|
+
:rtype: string
|
|
174
|
+
"""
|
|
175
|
+
if len(bin_str) != FileType.int_size:
|
|
176
|
+
raise errors.FileTypeFunctionError(
|
|
177
|
+
f"Method bin_to_str() take a {FileType.int_size} byte as input. {len(bin_str)} bytes given."
|
|
178
|
+
)
|
|
179
|
+
try:
|
|
180
|
+
integer_file_type = DataFile.bin_to_int(bin_str) # Convert the binary to integer
|
|
181
|
+
except ValueError:
|
|
182
|
+
raise errors.CryptoIncorrectFileType(
|
|
183
|
+
"8 first bits of the file are not an Integer. Cannot convert the 8bits to an integer."
|
|
184
|
+
)
|
|
185
|
+
for (key, val) in FileType.supported_types.items():
|
|
186
|
+
if val == integer_file_type:
|
|
187
|
+
return key
|
|
188
|
+
# Outside the loop means the value cannot be found. So raise an error
|
|
189
|
+
raise errors.CryptoIncorrectFileType(
|
|
190
|
+
f"Unknow integer. Cannot convert the integer to a known FileType. Given '{integer_file_type}'"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class DataFile:
|
|
195
|
+
"""
|
|
196
|
+
Class to compute information on the data file to compress (size)
|
|
197
|
+
"""
|
|
198
|
+
@staticmethod
|
|
199
|
+
def size_file_to_bin(input_file: Path) -> bytes:
|
|
200
|
+
"""
|
|
201
|
+
Compute size and return the value into a 8 Bytes binary representation
|
|
202
|
+
:param input_file: Path to the file to analyze
|
|
203
|
+
:type input_file: Path
|
|
204
|
+
:return: Size of the file as 8 Bytes bytes
|
|
205
|
+
:rtype: bytes
|
|
206
|
+
"""
|
|
207
|
+
file_size = input_file.stat().st_size
|
|
208
|
+
return DataFile.int_to_bin(file_size, 8)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def int_to_bin(integer: int, length: int = 1) -> bytes:
|
|
212
|
+
"""
|
|
213
|
+
Convert an integer to a binary representation with Big endian
|
|
214
|
+
|
|
215
|
+
:param integer: Int to convert to binary
|
|
216
|
+
:type integer: int
|
|
217
|
+
:param length: The length of the binary representation. 1 mean the integer
|
|
218
|
+
will be on 8bits (max 255)
|
|
219
|
+
:type length: int
|
|
220
|
+
:return: Binary representation of the integer
|
|
221
|
+
:rtype: bytes
|
|
222
|
+
"""
|
|
223
|
+
return (integer).to_bytes(length, "big")
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def bin_to_int(binary: bytes) -> int:
|
|
227
|
+
"""
|
|
228
|
+
Convert a byte into integer
|
|
229
|
+
|
|
230
|
+
:param binary: The byte(s) to convert
|
|
231
|
+
:type binary: bytes
|
|
232
|
+
:return: A integer
|
|
233
|
+
:rtype: int
|
|
234
|
+
"""
|
|
235
|
+
return int.from_bytes(binary, "big")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class SymmetricKey:
|
|
239
|
+
|
|
240
|
+
key_size = 32 # in bytes
|
|
241
|
+
iv_size = 16 # in bytes
|
|
242
|
+
|
|
243
|
+
def __init__(self, key: bytes, iv: bytes = None):
|
|
244
|
+
"""
|
|
245
|
+
Create a SymmetricKey object.
|
|
246
|
+
**Note**: You should use the method gen_key() instead of this constructor
|
|
247
|
+
|
|
248
|
+
:param key: Binary Key. The size have to match SymmetricKey.key_size
|
|
249
|
+
:type key: bytes
|
|
250
|
+
:param iv: Binary key to use as IV (mode). The size have to match SymmetricKey.iv_size
|
|
251
|
+
:return: SymmetricKey structure
|
|
252
|
+
:rtype: SymmetricKey
|
|
253
|
+
"""
|
|
254
|
+
self.key = key
|
|
255
|
+
self.iv = iv
|
|
256
|
+
self.cipher = Cipher(algorithms.AES(self.key), modes.CBC(self.iv))
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def from_binary_data(
|
|
260
|
+
encrypted_keys: bytes, private_key: "AsymmetricPrivateKey"
|
|
261
|
+
) -> "SymmetricKey":
|
|
262
|
+
"""
|
|
263
|
+
Decrypt symetric keys (key + iv) from binary data.
|
|
264
|
+
"""
|
|
265
|
+
if len(encrypted_keys) != AsymmetricPublicKey.key_size:
|
|
266
|
+
raise errors.DecryptSymmetricKeyError(
|
|
267
|
+
f"Wrong binary size. Should be {AsymmetricPublicKey.key_size}bytes large. You gave {len(encrypted_keys)}bytes"
|
|
268
|
+
)
|
|
269
|
+
decrypted_keys = private_key.decrypt(encrypted_keys)
|
|
270
|
+
key = decrypted_keys[0:SymmetricKey.key_size]
|
|
271
|
+
iv = decrypted_keys[SymmetricKey.key_size:]
|
|
272
|
+
return SymmetricKey(key=key, iv=iv)
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def gen_aes_key() -> "SymmetricKey":
|
|
276
|
+
"""
|
|
277
|
+
Generate a Cipher AES
|
|
278
|
+
|
|
279
|
+
:return: A structure
|
|
280
|
+
:rtype: cryptography.hazmat.primitives.ciphers.Cipher
|
|
281
|
+
"""
|
|
282
|
+
aes_key = os.urandom(SymmetricKey.key_size)
|
|
283
|
+
iv_key = os.urandom(SymmetricKey.iv_size)
|
|
284
|
+
return SymmetricKey(key=aes_key, iv=iv_key)
|
|
285
|
+
|
|
286
|
+
def encrypt(self, binary_txt: bytes) -> bytes:
|
|
287
|
+
encryptor = self.cipher.encryptor()
|
|
288
|
+
data, _ = fix_binary_data_length(binary_txt)
|
|
289
|
+
cipher_text = encryptor.update(data) + encryptor.finalize()
|
|
290
|
+
return cipher_text
|
|
291
|
+
|
|
292
|
+
def decrypt(self, binary_txt: bytes) -> bytes:
|
|
293
|
+
decryptor = self.cipher.decryptor()
|
|
294
|
+
data = decryptor.update(binary_txt)
|
|
295
|
+
return data
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class AsymmetricPublicKey:
|
|
299
|
+
"""
|
|
300
|
+
RSA public key
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
key_size = 256
|
|
304
|
+
padding = padding.OAEP(
|
|
305
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
306
|
+
algorithm=hashes.SHA256(),
|
|
307
|
+
label=None,
|
|
308
|
+
)
|
|
309
|
+
padding_size = 256
|
|
310
|
+
|
|
311
|
+
def __init__(self, public_key: RSAPublicKey):
|
|
312
|
+
|
|
313
|
+
if public_key.key_size / 8 != AsymmetricPublicKey.key_size:
|
|
314
|
+
# public_key.key_size is in bits and AsymmetricPublicKey.key_size in bytes
|
|
315
|
+
raise errors.AsymmetricKeyError(
|
|
316
|
+
f"RSA public key should have a size of 256 bits. Got {public_key.key_size}"
|
|
317
|
+
)
|
|
318
|
+
self.public_key = public_key
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def load_from_file(path_pub_key: Path) -> "AsymmetricPublicKey":
|
|
322
|
+
with open(path_pub_key, "rb") as buffer_pub_key:
|
|
323
|
+
pub_key = buffer_pub_key.read()
|
|
324
|
+
x509_key = x509.load_pem_x509_certificate(pub_key)
|
|
325
|
+
return AsymmetricPublicKey(x509_key.public_key())
|
|
326
|
+
|
|
327
|
+
def encrypt_symmetric_keys(self, symmetric_key: "SymmetricKey"):
|
|
328
|
+
data = symmetric_key.key + symmetric_key.iv
|
|
329
|
+
return self.encrypt(data)
|
|
330
|
+
|
|
331
|
+
def encrypt(self, binary_txt: bytes):
|
|
332
|
+
"""
|
|
333
|
+
Encrypt a bytes with the public key
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
encrypted_txt = self.public_key.encrypt(
|
|
337
|
+
binary_txt,
|
|
338
|
+
padding.OAEP(
|
|
339
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
340
|
+
algorithm=hashes.SHA256(),
|
|
341
|
+
label=None,
|
|
342
|
+
),
|
|
343
|
+
)
|
|
344
|
+
return encrypted_txt
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class AsymmetricPrivateKey:
|
|
348
|
+
"""
|
|
349
|
+
RSA private key
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
key_size = 256 # in bits
|
|
353
|
+
|
|
354
|
+
def __init__(self, private_key: RSAPrivateKey):
|
|
355
|
+
if private_key.key_size / 8 != AsymmetricPrivateKey.key_size:
|
|
356
|
+
# private_key.key_size is in bits and AsymmetricPrivateKey.key_size in bytes
|
|
357
|
+
raise errors.AsymmetricKeyError(
|
|
358
|
+
f"RSA private key should have a size of {AsymmetricPrivateKey.key_size} bytes. Got {private_key.key_size}"
|
|
359
|
+
)
|
|
360
|
+
self.private_key = private_key
|
|
361
|
+
|
|
362
|
+
def load_from_file(path_priv_key: Path) -> "AsymmetricPrivateKey":
|
|
363
|
+
with open(path_priv_key, "rb") as buffer_priv_key:
|
|
364
|
+
private_key = buffer_priv_key.read()
|
|
365
|
+
private_key = serialization.load_pem_private_key(private_key, password=None)
|
|
366
|
+
return AsymmetricPrivateKey(private_key)
|
|
367
|
+
|
|
368
|
+
def decrypt(self, binary_txt: bytes):
|
|
369
|
+
"""
|
|
370
|
+
Encrypt a bytes with the private key
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
decrypted_txt = self.private_key.decrypt(
|
|
374
|
+
binary_txt,
|
|
375
|
+
padding.OAEP(
|
|
376
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
377
|
+
algorithm=hashes.SHA256(),
|
|
378
|
+
label=None,
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
return decrypted_txt
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class Encrypt:
|
|
385
|
+
"""
|
|
386
|
+
Class used to ecrypt files according to format given in the
|
|
387
|
+
documentation's module
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def __init__(self, path_public_key: Path):
|
|
391
|
+
self.path_public_key = path_public_key
|
|
392
|
+
|
|
393
|
+
def encrypt_file(self, file_type: str, input_file: Path, output_file: Path):
|
|
394
|
+
"""
|
|
395
|
+
Method used to encrypt large file. The method is the same than encrypt_file but
|
|
396
|
+
it streams the input_file in chunk instead of whole file in memory.
|
|
397
|
+
|
|
398
|
+
:param file_type: type of the file. See documentation of FileType object for more information
|
|
399
|
+
:type file_type: str
|
|
400
|
+
:param input_file: Path to the file to encrypt
|
|
401
|
+
:type input_file: pathlib.Path
|
|
402
|
+
:param output_file: Path to the final encrypted file
|
|
403
|
+
:type output_file: pathlib.Path
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
buffer_output_file = open(output_file, "wb")
|
|
407
|
+
|
|
408
|
+
# convert file type into binary representation
|
|
409
|
+
binary_filetype = FileType.str_to_bin(file_type)
|
|
410
|
+
# Compute size of the file and get size into binary
|
|
411
|
+
binary_file_size: bytes = DataFile.size_file_to_bin(input_file)
|
|
412
|
+
# Generate symmetric key
|
|
413
|
+
symmetric_key = SymmetricKey.gen_aes_key()
|
|
414
|
+
# Load public key to encrypt symmetric key
|
|
415
|
+
public_key = AsymmetricPublicKey.load_from_file(self.path_public_key)
|
|
416
|
+
# Encrypt symmetric key
|
|
417
|
+
encrypted_keys = public_key.encrypt_symmetric_keys(symmetric_key)
|
|
418
|
+
# Write file type, symmetric key and size of the file in output file
|
|
419
|
+
buffer_output_file.write(binary_filetype + encrypted_keys + binary_file_size)
|
|
420
|
+
|
|
421
|
+
# Read input file by chunk, crypt every chunk and write them in output file
|
|
422
|
+
with open(input_file, "rb") as i_file:
|
|
423
|
+
while chunk := i_file.read(CHUNK_SIZE):
|
|
424
|
+
if len(chunk) != CHUNK_SIZE:
|
|
425
|
+
# Fix length of the last chunk by adding \'0
|
|
426
|
+
chunk, _ = fix_binary_data_length(chunk)
|
|
427
|
+
# Encrypt chunk and write it directly
|
|
428
|
+
encrypted_txt = symmetric_key.encrypt(chunk)
|
|
429
|
+
buffer_output_file.write(encrypted_txt)
|
|
430
|
+
finally:
|
|
431
|
+
buffer_output_file.close()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class Decrypt:
|
|
435
|
+
"""
|
|
436
|
+
Class used to decrypt file
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
def __init__(self, path_private_key: Path):
|
|
440
|
+
self.path_private_key = path_private_key
|
|
441
|
+
|
|
442
|
+
def decrypt_file(self, input_file: Path, output_file: Path) -> str:
|
|
443
|
+
"""
|
|
444
|
+
Method used to decrypt large file. The method is the same than decrypt_file but
|
|
445
|
+
it streams the input_file in chunk instead of whole file in memory.
|
|
446
|
+
|
|
447
|
+
:param input_file: Path to the file to encrypt
|
|
448
|
+
:type input_file: pathlib.Path
|
|
449
|
+
:param output_file: Path to the final encrypted file
|
|
450
|
+
:type output_file: pathlib.Path
|
|
451
|
+
:return: The string representation of the FileType ("file" or "xapi")
|
|
452
|
+
:rtype: str
|
|
453
|
+
"""
|
|
454
|
+
try:
|
|
455
|
+
buffer_output_file = open(output_file, "wb")
|
|
456
|
+
buffer_input_file = open(input_file, "rb")
|
|
457
|
+
# Load Asymmetric private key
|
|
458
|
+
private_key = AsymmetricPrivateKey.load_from_file(self.path_private_key)
|
|
459
|
+
# Read 8 first bytes for the FileType
|
|
460
|
+
file_type = buffer_input_file.read(FileType.int_size)
|
|
461
|
+
file_type = FileType.bin_to_str(file_type)
|
|
462
|
+
# Decrypt Symmetric key
|
|
463
|
+
encrypted_symmetric_keys = buffer_input_file.read(AsymmetricPublicKey.key_size)
|
|
464
|
+
symmetric_key = SymmetricKey.from_binary_data(encrypted_symmetric_keys, private_key)
|
|
465
|
+
# Decrypt the size of the file
|
|
466
|
+
binary_file_size = buffer_input_file.read(8)
|
|
467
|
+
file_size = DataFile.bin_to_int(binary_file_size)
|
|
468
|
+
|
|
469
|
+
# Read input file by chunk, crypt every chunk and write them in output file
|
|
470
|
+
while chunk := buffer_input_file.read(CHUNK_SIZE):
|
|
471
|
+
# Save chunk size here. After decryption, chunk variable is empty
|
|
472
|
+
chunk_size = len(chunk)
|
|
473
|
+
# Decrypt chunk
|
|
474
|
+
decrypted_txt = symmetric_key.decrypt(chunk)
|
|
475
|
+
if chunk_size != CHUNK_SIZE:
|
|
476
|
+
# remove trailing \'0 in the last chunk
|
|
477
|
+
decrypted_txt = sanitize_binary_data(decrypted_txt, file_size)
|
|
478
|
+
# Write chunk in file
|
|
479
|
+
buffer_output_file.write(decrypted_txt)
|
|
480
|
+
|
|
481
|
+
return file_type
|
|
482
|
+
finally:
|
|
483
|
+
buffer_input_file.close()
|
|
484
|
+
buffer_output_file.close()
|
cryptncompress/errors.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
class CryptErrors(Exception):
|
|
4
|
+
def __init__(self, message: str):
|
|
5
|
+
self.message = message
|
|
6
|
+
|
|
7
|
+
def __str__(self):
|
|
8
|
+
return str(self.message)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CryptoIncorrectFileType(CryptErrors):
|
|
12
|
+
"""
|
|
13
|
+
Error when parsing or converting the file type
|
|
14
|
+
"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileTypeToBinError(CryptErrors):
|
|
19
|
+
"""
|
|
20
|
+
Error when converting the integer associated to the FileType to binary 8bits
|
|
21
|
+
For exemple, if you want to convert 256 to bin => 100000000 so length is 9 and not 8
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FileTypeFunctionError(CryptErrors):
|
|
27
|
+
"""
|
|
28
|
+
Error when using function/methods for FileType in the wrong way
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DecryptSymmetricKeyError(CryptErrors):
|
|
34
|
+
"""
|
|
35
|
+
Error raised when decrypt Symmetric keys
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AsymmetricKeyError(CryptErrors):
|
|
41
|
+
"""
|
|
42
|
+
Error on RSA Public Key
|
|
43
|
+
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cryptncompress
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Encrypt/Decrypt, compress and Decompress files in Lola project
|
|
5
|
+
Author: Philippe N.
|
|
6
|
+
Author-email: philippe.noel@loria.fr
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: certifi==2026.4.22
|
|
10
|
+
Requires-Dist: cffi==2.0.0
|
|
11
|
+
Requires-Dist: cryptography==48.0.0
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: requires-python
|
|
17
|
+
Dynamic: summary
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cryptncompress/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
|
2
|
+
cryptncompress/compress.py,sha256=6R7_PKa8JPvc7DG-J2q_Cd9Bkf7u4sxaDGl609pL1-g,3819
|
|
3
|
+
cryptncompress/crypto.py,sha256=fhkhESUeY2rn51BRaDOiiTJTPjIivtYwLocSkREmKXg,18248
|
|
4
|
+
cryptncompress/errors.py,sha256=QhD3ppRQ1qD7XOcevhophHC3ypygT2Zr2InlLpyouK0,877
|
|
5
|
+
cryptncompress-2.0.0.dist-info/METADATA,sha256=CqNei5PSV4QjzhLhVfu_tS9ECNCT-njiVme57u6zOAk,479
|
|
6
|
+
cryptncompress-2.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
cryptncompress-2.0.0.dist-info/top_level.txt,sha256=8-Vtz0Xh--961wXQkxEI9St55KfxIz9tnZINugoNeh4,15
|
|
8
|
+
cryptncompress-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptncompress
|