cryptncompress 2.0.0__tar.gz

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.
@@ -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 @@
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)
@@ -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()
@@ -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,12 @@
1
+ requirements.txt
2
+ setup.py
3
+ cryptncompress/__init__.py
4
+ cryptncompress/compress.py
5
+ cryptncompress/crypto.py
6
+ cryptncompress/errors.py
7
+ cryptncompress.egg-info/PKG-INFO
8
+ cryptncompress.egg-info/SOURCES.txt
9
+ cryptncompress.egg-info/dependency_links.txt
10
+ cryptncompress.egg-info/requires.txt
11
+ cryptncompress.egg-info/top_level.txt
12
+ tests/test_crypto.py
@@ -0,0 +1,3 @@
1
+ certifi==2026.4.22
2
+ cffi==2.0.0
3
+ cryptography==48.0.0
@@ -0,0 +1 @@
1
+ cryptncompress
@@ -0,0 +1,3 @@
1
+ certifi==2026.4.22
2
+ cffi==2.0.0
3
+ cryptography==48.0.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ import setuptools
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def get_requirements():
7
+ """
8
+ Extract requirements from requirements.txt file
9
+
10
+ :return: a list of all requires
11
+ :rtype: list<str>
12
+ """
13
+ lib_folder = Path(os.path.realpath(__file__)).parent
14
+ requirement_path = lib_folder / "requirements.txt"
15
+ install_requires = []
16
+ if os.path.isfile(requirement_path):
17
+ with open(requirement_path) as f:
18
+ install_requires = f.read().splitlines()
19
+ return install_requires
20
+
21
+
22
+ setuptools.setup(
23
+ name="cryptncompress",
24
+ version="2.0.0",
25
+ author="Philippe N.",
26
+ author_email="philippe.noel@loria.fr",
27
+ description="Encrypt/Decrypt, compress and Decompress files in Lola project",
28
+ packages=setuptools.find_packages(),
29
+ install_requires=get_requirements(),
30
+ classifiers=[
31
+ "Programming Language :: Python :: 3",
32
+ ],
33
+ python_requires='>=3.12',
34
+ )
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import pytest
4
+
5
+ from cryptncompress.crypto import FileType, DataFile, SymmetricKey
6
+ from cryptncompress import errors
7
+ from cryptncompress.crypto import fix_binary_data_length
8
+
9
+
10
+ def test_fix_binary_data_length_block14():
11
+ # fix block of size 16. So with a block of size 14, It should
12
+ # add 2 bytes
13
+ my_data = b"0"*14
14
+ assert len(my_data) == 14
15
+ fixed_data, _ = fix_binary_data_length(my_data)
16
+ assert len(fixed_data) == SymmetricKey.key_size
17
+
18
+
19
+ def test_fix_binary_data_length_block17():
20
+ # fix block of size 16. So with a block of size 17, It should
21
+ # add 15 bytes
22
+ my_data = b"0"*17
23
+ assert len(my_data) == 17
24
+ fixed_data, _ = fix_binary_data_length(my_data)
25
+ assert len(fixed_data) == 32
26
+
27
+
28
+ def test_filetype_str_to_bin():
29
+ assert FileType.str_to_bin("file") == b"\x00"
30
+ assert FileType.str_to_bin("xapi") == b"\x01"
31
+
32
+
33
+ def test_filetype_str_to_bin_incorrectFileType():
34
+ # Should fail
35
+ with pytest.raises(errors.CryptoIncorrectFileType):
36
+ FileType.str_to_bin("toto")
37
+
38
+
39
+ def test_filetype_bin_to_str():
40
+ assert FileType.bin_to_str(b"\x00") == "file"
41
+ assert FileType.bin_to_str(b"\x01") == "xapi"
42
+
43
+
44
+ def test_filetype_bin_to_str_FileType_not_integer():
45
+ # Cannot convert the 8 first bits into an integer
46
+ # toto 2 times to avoid raising errors.FileTypeFunctionError
47
+ with pytest.raises(errors.FileTypeFunctionError):
48
+ FileType.bin_to_str(b"totototo")
49
+
50
+
51
+ def test_filetype_bin_to_str_unknowFileType():
52
+ # Cannot convert the 8 first bits into an integer
53
+ with pytest.raises(errors.FileTypeFunctionError):
54
+ FileType.bin_to_str(b"00000010")
55
+
56
+
57
+ def test_filetype_bin_to_str_wrongByteSize():
58
+ # Fail if binary size != 8bits
59
+ with pytest.raises(errors.FileTypeFunctionError):
60
+ FileType.bin_to_str(b"1"*5)
61
+ FileType.bin_to_str(b"1"*9)
62
+
63
+
64
+ def test_datafile_int_to_bin():
65
+ assert DataFile.int_to_bin(5, 1) == b"\x05"
66
+ assert DataFile.int_to_bin(5, 2) == b"\x00\x05"
67
+
68
+
69
+ def test_datafile_int_to_bin_overflow():
70
+ with pytest.raises(OverflowError):
71
+ DataFile.int_to_bin(256, 1)
72
+
73
+
74
+ def test_datafile_bin_to_int():
75
+ assert DataFile.bin_to_int(b"\x01"), 1
76
+ assert DataFile.bin_to_int(b"\x00\x00\x00\x00\x00\x01\xe2@"), 123456