aspose-cells-foss 25.12.1__py3-none-any.whl → 26.2.2__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.
Files changed (93) hide show
  1. aspose_cells/__init__.py +88 -0
  2. aspose_cells/auto_filter.py +527 -0
  3. aspose_cells/cell.py +483 -0
  4. aspose_cells/cell_value_handler.py +319 -0
  5. aspose_cells/cells.py +779 -0
  6. aspose_cells/cfb_handler.py +445 -0
  7. aspose_cells/cfb_writer.py +659 -0
  8. aspose_cells/cfb_writer_minimal.py +337 -0
  9. aspose_cells/comment_xml.py +475 -0
  10. aspose_cells/conditional_format.py +1185 -0
  11. aspose_cells/csv_handler.py +690 -0
  12. aspose_cells/data_validation.py +911 -0
  13. aspose_cells/document_properties.py +356 -0
  14. aspose_cells/encryption_crypto.py +247 -0
  15. aspose_cells/encryption_params.py +138 -0
  16. aspose_cells/hyperlink.py +372 -0
  17. aspose_cells/json_handler.py +185 -0
  18. aspose_cells/markdown_handler.py +583 -0
  19. aspose_cells/shared_strings.py +101 -0
  20. aspose_cells/style.py +841 -0
  21. aspose_cells/workbook.py +499 -0
  22. aspose_cells/workbook_hash_password.py +68 -0
  23. aspose_cells/workbook_properties.py +712 -0
  24. aspose_cells/worksheet.py +570 -0
  25. aspose_cells/worksheet_properties.py +1239 -0
  26. aspose_cells/xlsx_encryptor.py +403 -0
  27. aspose_cells/xml_autofilter_loader.py +195 -0
  28. aspose_cells/xml_autofilter_saver.py +173 -0
  29. aspose_cells/xml_conditional_format_loader.py +215 -0
  30. aspose_cells/xml_conditional_format_saver.py +351 -0
  31. aspose_cells/xml_datavalidation_loader.py +239 -0
  32. aspose_cells/xml_datavalidation_saver.py +245 -0
  33. aspose_cells/xml_hyperlink_handler.py +323 -0
  34. aspose_cells/xml_loader.py +986 -0
  35. aspose_cells/xml_properties_loader.py +512 -0
  36. aspose_cells/xml_properties_saver.py +607 -0
  37. aspose_cells/xml_saver.py +1306 -0
  38. aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
  39. aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
  40. {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
  41. aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
  42. aspose/__init__.py +0 -14
  43. aspose/cells/__init__.py +0 -31
  44. aspose/cells/cell.py +0 -350
  45. aspose/cells/constants.py +0 -44
  46. aspose/cells/converters/__init__.py +0 -13
  47. aspose/cells/converters/csv_converter.py +0 -55
  48. aspose/cells/converters/json_converter.py +0 -46
  49. aspose/cells/converters/markdown_converter.py +0 -453
  50. aspose/cells/drawing/__init__.py +0 -17
  51. aspose/cells/drawing/anchor.py +0 -172
  52. aspose/cells/drawing/collection.py +0 -233
  53. aspose/cells/drawing/image.py +0 -338
  54. aspose/cells/formats.py +0 -80
  55. aspose/cells/formula/__init__.py +0 -10
  56. aspose/cells/formula/evaluator.py +0 -360
  57. aspose/cells/formula/functions.py +0 -433
  58. aspose/cells/formula/tokenizer.py +0 -340
  59. aspose/cells/io/__init__.py +0 -27
  60. aspose/cells/io/csv/__init__.py +0 -8
  61. aspose/cells/io/csv/reader.py +0 -88
  62. aspose/cells/io/csv/writer.py +0 -98
  63. aspose/cells/io/factory.py +0 -138
  64. aspose/cells/io/interfaces.py +0 -48
  65. aspose/cells/io/json/__init__.py +0 -8
  66. aspose/cells/io/json/reader.py +0 -126
  67. aspose/cells/io/json/writer.py +0 -119
  68. aspose/cells/io/md/__init__.py +0 -8
  69. aspose/cells/io/md/reader.py +0 -161
  70. aspose/cells/io/md/writer.py +0 -334
  71. aspose/cells/io/models.py +0 -64
  72. aspose/cells/io/xlsx/__init__.py +0 -9
  73. aspose/cells/io/xlsx/constants.py +0 -312
  74. aspose/cells/io/xlsx/image_writer.py +0 -311
  75. aspose/cells/io/xlsx/reader.py +0 -284
  76. aspose/cells/io/xlsx/writer.py +0 -931
  77. aspose/cells/plugins/__init__.py +0 -6
  78. aspose/cells/plugins/docling_backend/__init__.py +0 -7
  79. aspose/cells/plugins/docling_backend/backend.py +0 -535
  80. aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
  81. aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
  82. aspose/cells/range.py +0 -210
  83. aspose/cells/style.py +0 -287
  84. aspose/cells/utils/__init__.py +0 -54
  85. aspose/cells/utils/coordinates.py +0 -68
  86. aspose/cells/utils/exceptions.py +0 -43
  87. aspose/cells/utils/validation.py +0 -102
  88. aspose/cells/workbook.py +0 -352
  89. aspose/cells/worksheet.py +0 -670
  90. aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
  91. aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
  92. aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
  93. aspose_cells_foss-25.12.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,445 @@
1
+ """
2
+ Compound File Binary (CFB) Format Handler
3
+
4
+ This module handles reading and writing CFB (also known as OLE) format files
5
+ used for encrypted XLSX packages according to ECMA-376.
6
+ """
7
+
8
+ import struct
9
+ import base64
10
+ import xml.etree.ElementTree as ET
11
+ from pathlib import Path
12
+
13
+ from .encryption_params import HashAlgorithm, CipherAlgorithm, EncryptionType
14
+ from .cfb_writer import CFBWriter as CFBWriterImpl
15
+
16
+
17
+ class CFBReader:
18
+ """
19
+ Reads encrypted XLSX from CFB format.
20
+ """
21
+
22
+ def __init__(self, file_path):
23
+ """
24
+ Initialize CFB reader.
25
+
26
+ Args:
27
+ file_path: Path to CFB file
28
+ """
29
+ self.file_path = file_path
30
+ self._fh = open(file_path, 'rb')
31
+ self._load_header()
32
+ self._load_fat()
33
+ self._load_directory()
34
+
35
+ def read_encryption_info(self):
36
+ """
37
+ Read EncryptionInfo stream from CFB file.
38
+
39
+ Returns:
40
+ dict with encryption parameters
41
+ """
42
+ data = self._read_stream('EncryptionInfo')
43
+ if data is None:
44
+ raise ValueError("EncryptionInfo stream not found in CFB file")
45
+
46
+ # Strip null padding from sector/mini-sector alignment.
47
+ data = data.rstrip(b'\x00')
48
+
49
+ # Read version info
50
+ version_major, version_minor = struct.unpack('<HH', data[0:4])
51
+ flags = struct.unpack('<I', data[4:8])[0]
52
+
53
+ if version_major == 4 and version_minor == 4:
54
+ # Agile Encryption
55
+ return self._parse_agile_encryption_info(data[8:])
56
+ elif version_major in (2, 3, 4) and version_minor == 2:
57
+ # Standard Encryption
58
+ return self._parse_standard_encryption_info(data, flags)
59
+ else:
60
+ raise ValueError(f"Unsupported encryption version: {version_major}.{version_minor}")
61
+
62
+ def _parse_agile_encryption_info(self, xml_data):
63
+ """
64
+ Parse Agile encryption info from XML.
65
+
66
+ Args:
67
+ xml_data: XML descriptor bytes
68
+
69
+ Returns:
70
+ dict with encryption parameters
71
+ """
72
+ # Parse XML
73
+ root = ET.fromstring(xml_data.decode('utf-8'))
74
+
75
+ # Define namespace
76
+ ns = {'enc': 'http://schemas.microsoft.com/office/2006/encryption',
77
+ 'p': 'http://schemas.microsoft.com/office/2006/keyEncryptor/password'}
78
+
79
+ # Extract keyData
80
+ key_data = root.find('enc:keyData', ns)
81
+ if key_data is None:
82
+ raise ValueError("keyData element not found in Agile encryption info")
83
+
84
+ cipher_algorithm_name = key_data.get('cipherAlgorithm', 'AES')
85
+ key_bits = int(key_data.get('keyBits', '256'))
86
+ hash_algorithm_name = key_data.get('hashAlgorithm', 'SHA512')
87
+ salt_value = base64.b64decode(key_data.get('saltValue', ''))
88
+
89
+ # Extract encryptedKey from keyEncryptors
90
+ encrypted_key_elem = root.find('.//p:encryptedKey', ns)
91
+ if encrypted_key_elem is None:
92
+ raise ValueError("encryptedKey element not found")
93
+
94
+ spin_count = int(encrypted_key_elem.get('spinCount', '100000'))
95
+ key_salt = base64.b64decode(encrypted_key_elem.get('saltValue', ''))
96
+ encrypted_verifier = base64.b64decode(encrypted_key_elem.get('encryptedVerifierHashInput', ''))
97
+ encrypted_verifier_hash = base64.b64decode(encrypted_key_elem.get('encryptedVerifierHashValue', ''))
98
+ encrypted_key_value = base64.b64decode(encrypted_key_elem.get('encryptedKeyValue', ''))
99
+
100
+ data_integrity = root.find('enc:dataIntegrity', ns)
101
+ encrypted_hmac_key = b''
102
+ encrypted_hmac_value = b''
103
+ if data_integrity is not None:
104
+ encrypted_hmac_key = base64.b64decode(data_integrity.get('encryptedHmacKey', '') or b'')
105
+ encrypted_hmac_value = base64.b64decode(data_integrity.get('encryptedHmacValue', '') or b'')
106
+
107
+ # Map algorithm names to enums
108
+ cipher_algorithm = self._map_cipher_algorithm(cipher_algorithm_name, key_bits)
109
+ hash_algorithm = self._map_hash_algorithm(hash_algorithm_name)
110
+
111
+ return {
112
+ 'type': EncryptionType.AGILE,
113
+ 'cipher_algorithm': cipher_algorithm,
114
+ 'hash_algorithm': hash_algorithm,
115
+ 'spin_count': spin_count,
116
+ 'salt': key_salt,
117
+ 'encrypted_verifier': encrypted_verifier,
118
+ 'encrypted_verifier_hash': encrypted_verifier_hash,
119
+ 'encrypted_key_value': encrypted_key_value,
120
+ 'encrypted_hmac_key': encrypted_hmac_key,
121
+ 'encrypted_hmac_value': encrypted_hmac_value,
122
+ 'package_salt': salt_value
123
+ }
124
+
125
+ def _parse_standard_encryption_info(self, data, flags):
126
+ """
127
+ Parse Standard encryption info from binary data.
128
+
129
+ Args:
130
+ data: Full EncryptionInfo data
131
+ flags: Flags value
132
+
133
+ Returns:
134
+ dict with encryption parameters
135
+ """
136
+ # This is a simplified parser for Standard encryption
137
+ # Full implementation would need to parse the complete binary structure
138
+ raise NotImplementedError("Standard encryption is not yet supported. "
139
+ "Please use Agile encryption (Office 2010+)")
140
+
141
+ def _map_cipher_algorithm(self, name, key_bits):
142
+ """Map cipher algorithm name and key bits to enum."""
143
+ if name == 'AES':
144
+ if key_bits == 128:
145
+ return CipherAlgorithm.AES_128
146
+ elif key_bits == 192:
147
+ return CipherAlgorithm.AES_192
148
+ elif key_bits == 256:
149
+ return CipherAlgorithm.AES_256
150
+ raise ValueError(f"Unsupported cipher algorithm: {name}-{key_bits}")
151
+
152
+ def _map_hash_algorithm(self, name):
153
+ """Map hash algorithm name to enum."""
154
+ name_upper = name.upper().replace('-', '')
155
+ if name_upper == 'SHA1':
156
+ return HashAlgorithm.SHA1
157
+ elif name_upper == 'SHA256':
158
+ return HashAlgorithm.SHA256
159
+ elif name_upper == 'SHA384':
160
+ return HashAlgorithm.SHA384
161
+ elif name_upper == 'SHA512':
162
+ return HashAlgorithm.SHA512
163
+ raise ValueError(f"Unsupported hash algorithm: {name}")
164
+
165
+ def read_encrypted_package(self):
166
+ """
167
+ Read EncryptedPackage stream from CFB file.
168
+
169
+ Returns:
170
+ bytes: Encrypted package data
171
+ """
172
+ data = self._read_stream_raw('EncryptedPackage')
173
+ if data is None:
174
+ raise ValueError("EncryptedPackage stream not found in CFB file")
175
+
176
+ # Read package size (first 8 bytes as uint64)
177
+ size_bytes = data[:8]
178
+ package_size = struct.unpack('<Q', size_bytes)[0]
179
+ encrypted_payload = data[8:]
180
+ return package_size, encrypted_payload, data
181
+
182
+ def close(self):
183
+ """Close the CFB file."""
184
+ if self._fh:
185
+ self._fh.close()
186
+
187
+ def __enter__(self):
188
+ return self
189
+
190
+ def __exit__(self, exc_type, exc_val, exc_tb):
191
+ self.close()
192
+
193
+ def _load_header(self):
194
+ self._fh.seek(0)
195
+ header = self._fh.read(512)
196
+ sig = struct.unpack('<Q', header[0:8])[0]
197
+ if sig != 0xE11AB1A1E011CFD0:
198
+ raise ValueError("Invalid CFB signature")
199
+
200
+ self.sector_shift = struct.unpack('<H', header[30:32])[0]
201
+ self.sector_size = 1 << self.sector_shift
202
+ self.mini_sector_size = 1 << struct.unpack('<H', header[32:34])[0]
203
+ self.num_fat_sectors = struct.unpack('<I', header[44:48])[0]
204
+ self.dir_start = struct.unpack('<I', header[48:52])[0]
205
+ self.mini_stream_cutoff = struct.unpack('<I', header[56:60])[0]
206
+ self.mini_fat_start = struct.unpack('<I', header[60:64])[0]
207
+ self.num_mini_fat_sectors = struct.unpack('<I', header[64:68])[0]
208
+
209
+ # DIFAT entries (first 109)
210
+ self.difat = []
211
+ for i in range(109):
212
+ entry = struct.unpack('<I', header[76 + i * 4: 80 + i * 4])[0]
213
+ if entry != 0xFFFFFFFF:
214
+ self.difat.append(entry)
215
+
216
+ def _read_sector(self, sector_index):
217
+ self._fh.seek(512 + sector_index * self.sector_size)
218
+ return self._fh.read(self.sector_size)
219
+
220
+ def _load_fat(self):
221
+ fat_entries = []
222
+ for i in range(self.num_fat_sectors):
223
+ sector = self.difat[i]
224
+ data = self._read_sector(sector)
225
+ for j in range(0, len(data), 4):
226
+ fat_entries.append(struct.unpack('<I', data[j:j + 4])[0])
227
+ self.fat = fat_entries
228
+
229
+ def _load_directory(self):
230
+ dir_data = self._read_stream_by_chain(self.dir_start, is_mini=False)
231
+ entries = []
232
+ for i in range(0, len(dir_data), 128):
233
+ entry = dir_data[i:i + 128]
234
+ if len(entry) < 128:
235
+ break
236
+ name_len = struct.unpack('<H', entry[64:66])[0]
237
+ name_bytes = entry[0:name_len - 2] if name_len >= 2 else b''
238
+ name = name_bytes.decode('utf-16le', errors='ignore')
239
+ obj_type = entry[66]
240
+ color = entry[67]
241
+ left = struct.unpack('<I', entry[68:72])[0]
242
+ right = struct.unpack('<I', entry[72:76])[0]
243
+ child = struct.unpack('<I', entry[76:80])[0]
244
+ starting_sector = struct.unpack('<I', entry[116:120])[0]
245
+ size = struct.unpack('<Q', entry[120:128])[0]
246
+ entries.append({
247
+ 'name': name,
248
+ 'type': obj_type,
249
+ 'color': color,
250
+ 'left': left,
251
+ 'right': right,
252
+ 'child': child,
253
+ 'start': starting_sector,
254
+ 'size': size,
255
+ })
256
+ self.dir_entries = entries
257
+
258
+ # Build name->entry index mapping with path traversal
259
+ self.name_map = {}
260
+ def visit(index, parent_path):
261
+ if index == 0xFFFFFFFF or index >= len(self.dir_entries):
262
+ return
263
+ entry = self.dir_entries[index]
264
+ visit(entry['left'], parent_path)
265
+ name = entry['name']
266
+ path = name if not parent_path else parent_path + '/' + name
267
+ self.name_map[path] = index
268
+ if entry['type'] in (1, 5): # storage or root
269
+ visit(entry['child'], path if entry['type'] == 1 else "")
270
+ visit(entry['right'], parent_path)
271
+ visit(0, "")
272
+
273
+ def _read_stream(self, name):
274
+ index = self.name_map.get(name)
275
+ if index is None:
276
+ return None
277
+ entry = self.dir_entries[index]
278
+ size = entry['size']
279
+ if size == 0:
280
+ return b''
281
+ if size < self.mini_stream_cutoff and entry['type'] == 2:
282
+ return self._read_mini_stream(entry['start'], size)
283
+ return self._read_stream_by_chain(entry['start'], is_mini=False, size=size)
284
+
285
+ def _read_stream_raw(self, name):
286
+ index = self.name_map.get(name)
287
+ if index is None:
288
+ return None
289
+ entry = self.dir_entries[index]
290
+ size = entry['size']
291
+ if size == 0:
292
+ return b''
293
+ if size < self.mini_stream_cutoff and entry['type'] == 2:
294
+ return self._read_mini_stream(entry['start'], size)
295
+ data = self._read_stream_by_chain(entry['start'], is_mini=False, size=None)
296
+ return data[:size]
297
+
298
+ def _read_stream_by_chain(self, start_sector, is_mini=False, size=None):
299
+ data = bytearray()
300
+ sector = start_sector
301
+ while sector not in (0xFFFFFFFE, 0xFFFFFFFF):
302
+ data.extend(self._read_sector(sector))
303
+ sector = self.fat[sector]
304
+ if size is not None:
305
+ return bytes(data[:size])
306
+ return bytes(data)
307
+
308
+ def _load_minifat(self):
309
+ if self.num_mini_fat_sectors == 0:
310
+ self.mini_fat = []
311
+ return
312
+ data = bytearray()
313
+ sector = self.mini_fat_start
314
+ for _ in range(self.num_mini_fat_sectors):
315
+ data.extend(self._read_sector(sector))
316
+ sector = self.fat[sector]
317
+ self.mini_fat = []
318
+ for i in range(0, len(data), 4):
319
+ self.mini_fat.append(struct.unpack('<I', data[i:i + 4])[0])
320
+
321
+ def _read_mini_stream(self, start_mini_sector, size):
322
+ if not hasattr(self, 'mini_fat'):
323
+ self._load_minifat()
324
+ # root entry is 0
325
+ root = self.dir_entries[0]
326
+ mini_stream = self._read_stream_by_chain(root['start'], is_mini=False, size=root['size'])
327
+ data = bytearray()
328
+ mini_sector = start_mini_sector
329
+ while mini_sector not in (0xFFFFFFFE, 0xFFFFFFFF):
330
+ offset = mini_sector * self.mini_sector_size
331
+ data.extend(mini_stream[offset:offset + self.mini_sector_size])
332
+ mini_sector = self.mini_fat[mini_sector]
333
+ return bytes(data[:size])
334
+
335
+
336
+ class CFBWriter:
337
+ """
338
+ Writes encrypted XLSX to CFB format.
339
+ """
340
+
341
+ def __init__(self):
342
+ """Initialize CFB writer."""
343
+ pass
344
+
345
+ def write(self, file_path, encryption_info_xml, encrypted_package, package_size):
346
+ """
347
+ Write CFB file with EncryptionInfo and EncryptedPackage streams.
348
+
349
+ Args:
350
+ file_path: Output file path
351
+ encryption_info_xml: EncryptionInfo XML bytes
352
+ encrypted_package: Encrypted package bytes
353
+ package_size: Original (unencrypted) package size in bytes
354
+ """
355
+ # Prepare stream data
356
+ # EncryptionInfo: Version + Flags + XML
357
+ version_info = struct.pack('<HH', 4, 4) # Major=4, Minor=4
358
+ flags = struct.pack('<I', 0x40)
359
+ encryption_info_data = version_info + flags + encryption_info_xml
360
+
361
+ # EncryptedPackage: Size + Data
362
+ package_size_bytes = struct.pack('<Q', package_size)
363
+ encrypted_package_data = package_size_bytes + encrypted_package
364
+
365
+ # Create CFB file using MS-CFB compliant writer
366
+ # Use 512-byte sectors (version 3) for maximum compatibility with olefile
367
+ writer = CFBWriterImpl(sector_size=512)
368
+ # Allow CFB writer to place small streams in the mini stream.
369
+ writer.add_stream('EncryptionInfo', encryption_info_data)
370
+ writer.add_stream('EncryptedPackage', encrypted_package_data)
371
+ for name, data in _build_dataspaces_streams().items():
372
+ writer.add_stream(name, data)
373
+ writer.write(file_path)
374
+
375
+
376
+ def _build_dataspaces_streams():
377
+ """
378
+ Build DataSpaces streams required by Office for Agile encryption.
379
+ """
380
+ def _unicode_lp_p4(text):
381
+ data = text.encode('utf-16le')
382
+ return struct.pack('<I', len(data)) + data
383
+
384
+ def _pad4(data):
385
+ pad = (-len(data)) % 4
386
+ if pad:
387
+ return data + (b'\x00' * pad)
388
+ return data
389
+
390
+ # Version stream
391
+ version_name = "Microsoft.Container.DataSpaces"
392
+ version_data = _unicode_lp_p4(version_name) + struct.pack('<III', 1, 1, 1)
393
+
394
+ # DataSpaceMap stream
395
+ component_name = "EncryptedPackage"
396
+ dataspace_name = "StrongEncryptionDataSpace"
397
+ entry = struct.pack('<I', 1) # cRefComponents
398
+ entry += struct.pack('<I', 0) # ReferenceComponentType = Stream (0)
399
+ entry += _unicode_lp_p4(component_name)
400
+ entry += _unicode_lp_p4(dataspace_name)
401
+ entry = _pad4(entry)
402
+ # Excel sets entry length to 104 even though entry payload is 100 bytes.
403
+ # Match that for compatibility.
404
+ data_space_map = struct.pack('<II', 8, 1) + struct.pack('<I', len(entry) + 4) + entry
405
+
406
+ # DataSpaceInfo stream
407
+ transform_name = "StrongEncryptionTransform"
408
+ ds_info = struct.pack('<II', 8, 1) + _pad4(_unicode_lp_p4(transform_name))
409
+
410
+ # TransformInfo Primary stream
411
+ transform_id = "{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}"
412
+ transform_type = "Microsoft.Container.EncryptionTransform"
413
+ primary = struct.pack('<II', 88, 1)
414
+ primary += _unicode_lp_p4(transform_id)
415
+ primary += _unicode_lp_p4(transform_type)
416
+ primary += bytes.fromhex(
417
+ "000001000000010000000100000000000000000000000000000004000000"
418
+ )
419
+
420
+ return {
421
+ "\x06DataSpaces/Version": version_data,
422
+ "\x06DataSpaces/DataSpaceMap": data_space_map,
423
+ "\x06DataSpaces/DataSpaceInfo/StrongEncryptionDataSpace": ds_info,
424
+ "\x06DataSpaces/TransformInfo/StrongEncryptionTransform/\x06Primary": primary,
425
+ }
426
+
427
+
428
+ def is_encrypted_file(file_path):
429
+ """
430
+ Check if a file is an encrypted XLSX (CFB format).
431
+
432
+ Args:
433
+ file_path: Path to file
434
+
435
+ Returns:
436
+ bool: True if file is encrypted (CFB format)
437
+ """
438
+ try:
439
+ with open(file_path, 'rb') as f:
440
+ # Check for CFB signature (first 8 bytes)
441
+ header = f.read(8)
442
+ # CFB signature: D0 CF 11 E0 A1 B1 1A E1
443
+ return header == b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1'
444
+ except Exception:
445
+ return False