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.
- aspose_cells/__init__.py +88 -0
- aspose_cells/auto_filter.py +527 -0
- aspose_cells/cell.py +483 -0
- aspose_cells/cell_value_handler.py +319 -0
- aspose_cells/cells.py +779 -0
- aspose_cells/cfb_handler.py +445 -0
- aspose_cells/cfb_writer.py +659 -0
- aspose_cells/cfb_writer_minimal.py +337 -0
- aspose_cells/comment_xml.py +475 -0
- aspose_cells/conditional_format.py +1185 -0
- aspose_cells/csv_handler.py +690 -0
- aspose_cells/data_validation.py +911 -0
- aspose_cells/document_properties.py +356 -0
- aspose_cells/encryption_crypto.py +247 -0
- aspose_cells/encryption_params.py +138 -0
- aspose_cells/hyperlink.py +372 -0
- aspose_cells/json_handler.py +185 -0
- aspose_cells/markdown_handler.py +583 -0
- aspose_cells/shared_strings.py +101 -0
- aspose_cells/style.py +841 -0
- aspose_cells/workbook.py +499 -0
- aspose_cells/workbook_hash_password.py +68 -0
- aspose_cells/workbook_properties.py +712 -0
- aspose_cells/worksheet.py +570 -0
- aspose_cells/worksheet_properties.py +1239 -0
- aspose_cells/xlsx_encryptor.py +403 -0
- aspose_cells/xml_autofilter_loader.py +195 -0
- aspose_cells/xml_autofilter_saver.py +173 -0
- aspose_cells/xml_conditional_format_loader.py +215 -0
- aspose_cells/xml_conditional_format_saver.py +351 -0
- aspose_cells/xml_datavalidation_loader.py +239 -0
- aspose_cells/xml_datavalidation_saver.py +245 -0
- aspose_cells/xml_hyperlink_handler.py +323 -0
- aspose_cells/xml_loader.py +986 -0
- aspose_cells/xml_properties_loader.py +512 -0
- aspose_cells/xml_properties_saver.py +607 -0
- aspose_cells/xml_saver.py +1306 -0
- aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
- aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
- {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
- aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
- aspose/__init__.py +0 -14
- aspose/cells/__init__.py +0 -31
- aspose/cells/cell.py +0 -350
- aspose/cells/constants.py +0 -44
- aspose/cells/converters/__init__.py +0 -13
- aspose/cells/converters/csv_converter.py +0 -55
- aspose/cells/converters/json_converter.py +0 -46
- aspose/cells/converters/markdown_converter.py +0 -453
- aspose/cells/drawing/__init__.py +0 -17
- aspose/cells/drawing/anchor.py +0 -172
- aspose/cells/drawing/collection.py +0 -233
- aspose/cells/drawing/image.py +0 -338
- aspose/cells/formats.py +0 -80
- aspose/cells/formula/__init__.py +0 -10
- aspose/cells/formula/evaluator.py +0 -360
- aspose/cells/formula/functions.py +0 -433
- aspose/cells/formula/tokenizer.py +0 -340
- aspose/cells/io/__init__.py +0 -27
- aspose/cells/io/csv/__init__.py +0 -8
- aspose/cells/io/csv/reader.py +0 -88
- aspose/cells/io/csv/writer.py +0 -98
- aspose/cells/io/factory.py +0 -138
- aspose/cells/io/interfaces.py +0 -48
- aspose/cells/io/json/__init__.py +0 -8
- aspose/cells/io/json/reader.py +0 -126
- aspose/cells/io/json/writer.py +0 -119
- aspose/cells/io/md/__init__.py +0 -8
- aspose/cells/io/md/reader.py +0 -161
- aspose/cells/io/md/writer.py +0 -334
- aspose/cells/io/models.py +0 -64
- aspose/cells/io/xlsx/__init__.py +0 -9
- aspose/cells/io/xlsx/constants.py +0 -312
- aspose/cells/io/xlsx/image_writer.py +0 -311
- aspose/cells/io/xlsx/reader.py +0 -284
- aspose/cells/io/xlsx/writer.py +0 -931
- aspose/cells/plugins/__init__.py +0 -6
- aspose/cells/plugins/docling_backend/__init__.py +0 -7
- aspose/cells/plugins/docling_backend/backend.py +0 -535
- aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
- aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
- aspose/cells/range.py +0 -210
- aspose/cells/style.py +0 -287
- aspose/cells/utils/__init__.py +0 -54
- aspose/cells/utils/coordinates.py +0 -68
- aspose/cells/utils/exceptions.py +0 -43
- aspose/cells/utils/validation.py +0 -102
- aspose/cells/workbook.py +0 -352
- aspose/cells/worksheet.py +0 -670
- aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
- aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aspose.Cells for Python - Document Properties Module
|
|
3
|
+
|
|
4
|
+
This module provides classes for document-level properties according to ECMA-376 specification.
|
|
5
|
+
These properties are stored in docProps/core.xml and docProps/app.xml files.
|
|
6
|
+
|
|
7
|
+
ECMA-376 Part 2: Open Packaging Conventions - Core Properties
|
|
8
|
+
ECMA-376 Part 1: Extended Properties (docProps/app.xml)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CoreProperties:
|
|
15
|
+
"""
|
|
16
|
+
Represents core document properties stored in docProps/core.xml.
|
|
17
|
+
|
|
18
|
+
Uses Dublin Core metadata elements and OPC core properties.
|
|
19
|
+
|
|
20
|
+
ECMA-376 Part 2, Section 11 - Core Properties
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> wb.document_properties.core.title = "Sales Report"
|
|
24
|
+
>>> wb.document_properties.core.creator = "John Doe"
|
|
25
|
+
>>> wb.document_properties.core.subject = "Q4 2024 Sales"
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self._title = None
|
|
30
|
+
self._subject = None
|
|
31
|
+
self._creator = None
|
|
32
|
+
self._keywords = None
|
|
33
|
+
self._description = None
|
|
34
|
+
self._last_modified_by = None
|
|
35
|
+
self._revision = None
|
|
36
|
+
self._created = None
|
|
37
|
+
self._modified = None
|
|
38
|
+
self._category = None
|
|
39
|
+
self._content_status = None
|
|
40
|
+
self._content_type = None
|
|
41
|
+
self._identifier = None
|
|
42
|
+
self._language = None
|
|
43
|
+
self._version = None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def title(self):
|
|
47
|
+
"""Document title (dc:title)."""
|
|
48
|
+
return self._title
|
|
49
|
+
|
|
50
|
+
@title.setter
|
|
51
|
+
def title(self, value):
|
|
52
|
+
self._title = value
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def subject(self):
|
|
56
|
+
"""Document subject (dc:subject)."""
|
|
57
|
+
return self._subject
|
|
58
|
+
|
|
59
|
+
@subject.setter
|
|
60
|
+
def subject(self, value):
|
|
61
|
+
self._subject = value
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def creator(self):
|
|
65
|
+
"""Document creator/author (dc:creator)."""
|
|
66
|
+
return self._creator
|
|
67
|
+
|
|
68
|
+
@creator.setter
|
|
69
|
+
def creator(self, value):
|
|
70
|
+
self._creator = value
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def keywords(self):
|
|
74
|
+
"""Keywords associated with the document (cp:keywords)."""
|
|
75
|
+
return self._keywords
|
|
76
|
+
|
|
77
|
+
@keywords.setter
|
|
78
|
+
def keywords(self, value):
|
|
79
|
+
self._keywords = value
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def description(self):
|
|
83
|
+
"""Document description/comments (dc:description)."""
|
|
84
|
+
return self._description
|
|
85
|
+
|
|
86
|
+
@description.setter
|
|
87
|
+
def description(self, value):
|
|
88
|
+
self._description = value
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def last_modified_by(self):
|
|
92
|
+
"""Name of person who last modified the document (cp:lastModifiedBy)."""
|
|
93
|
+
return self._last_modified_by
|
|
94
|
+
|
|
95
|
+
@last_modified_by.setter
|
|
96
|
+
def last_modified_by(self, value):
|
|
97
|
+
self._last_modified_by = value
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def revision(self):
|
|
101
|
+
"""Revision number (cp:revision)."""
|
|
102
|
+
return self._revision
|
|
103
|
+
|
|
104
|
+
@revision.setter
|
|
105
|
+
def revision(self, value):
|
|
106
|
+
self._revision = value
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def created(self):
|
|
110
|
+
"""Document creation date (dcterms:created)."""
|
|
111
|
+
return self._created
|
|
112
|
+
|
|
113
|
+
@created.setter
|
|
114
|
+
def created(self, value):
|
|
115
|
+
self._created = value
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def modified(self):
|
|
119
|
+
"""Document last modification date (dcterms:modified)."""
|
|
120
|
+
return self._modified
|
|
121
|
+
|
|
122
|
+
@modified.setter
|
|
123
|
+
def modified(self, value):
|
|
124
|
+
self._modified = value
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def category(self):
|
|
128
|
+
"""Document category (cp:category)."""
|
|
129
|
+
return self._category
|
|
130
|
+
|
|
131
|
+
@category.setter
|
|
132
|
+
def category(self, value):
|
|
133
|
+
self._category = value
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def content_status(self):
|
|
137
|
+
"""Content status such as Draft, Final (cp:contentStatus)."""
|
|
138
|
+
return self._content_status
|
|
139
|
+
|
|
140
|
+
@content_status.setter
|
|
141
|
+
def content_status(self, value):
|
|
142
|
+
self._content_status = value
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ExtendedProperties:
|
|
146
|
+
"""
|
|
147
|
+
Represents extended/application properties stored in docProps/app.xml.
|
|
148
|
+
|
|
149
|
+
ECMA-376 Part 1, Section 22.2 - Extended Properties
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
>>> wb.document_properties.extended.application = "Microsoft Excel"
|
|
153
|
+
>>> wb.document_properties.extended.company = "Acme Corp"
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self):
|
|
157
|
+
self._application = "Microsoft Excel"
|
|
158
|
+
self._app_version = None
|
|
159
|
+
self._company = None
|
|
160
|
+
self._manager = None
|
|
161
|
+
self._doc_security = 0
|
|
162
|
+
self._hyperlink_base = None
|
|
163
|
+
self._scale_crop = False
|
|
164
|
+
self._links_up_to_date = False
|
|
165
|
+
self._shared_doc = False
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def application(self):
|
|
169
|
+
"""Name of the application that created the document."""
|
|
170
|
+
return self._application
|
|
171
|
+
|
|
172
|
+
@application.setter
|
|
173
|
+
def application(self, value):
|
|
174
|
+
self._application = value
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def app_version(self):
|
|
178
|
+
"""Version of the application that created the document."""
|
|
179
|
+
return self._app_version
|
|
180
|
+
|
|
181
|
+
@app_version.setter
|
|
182
|
+
def app_version(self, value):
|
|
183
|
+
self._app_version = value
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def company(self):
|
|
187
|
+
"""Company or organization name."""
|
|
188
|
+
return self._company
|
|
189
|
+
|
|
190
|
+
@company.setter
|
|
191
|
+
def company(self, value):
|
|
192
|
+
self._company = value
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def manager(self):
|
|
196
|
+
"""Manager associated with the document."""
|
|
197
|
+
return self._manager
|
|
198
|
+
|
|
199
|
+
@manager.setter
|
|
200
|
+
def manager(self, value):
|
|
201
|
+
self._manager = value
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def doc_security(self):
|
|
205
|
+
"""Document security level (0=none, 1=password protected, etc.)."""
|
|
206
|
+
return self._doc_security
|
|
207
|
+
|
|
208
|
+
@doc_security.setter
|
|
209
|
+
def doc_security(self, value):
|
|
210
|
+
self._doc_security = value
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def hyperlink_base(self):
|
|
214
|
+
"""Base URL for relative hyperlinks."""
|
|
215
|
+
return self._hyperlink_base
|
|
216
|
+
|
|
217
|
+
@hyperlink_base.setter
|
|
218
|
+
def hyperlink_base(self, value):
|
|
219
|
+
self._hyperlink_base = value
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def scale_crop(self):
|
|
223
|
+
"""Whether to scale or crop document thumbnail."""
|
|
224
|
+
return self._scale_crop
|
|
225
|
+
|
|
226
|
+
@scale_crop.setter
|
|
227
|
+
def scale_crop(self, value):
|
|
228
|
+
self._scale_crop = value
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def links_up_to_date(self):
|
|
232
|
+
"""Whether hyperlinks are up to date."""
|
|
233
|
+
return self._links_up_to_date
|
|
234
|
+
|
|
235
|
+
@links_up_to_date.setter
|
|
236
|
+
def links_up_to_date(self, value):
|
|
237
|
+
self._links_up_to_date = value
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def shared_doc(self):
|
|
241
|
+
"""Whether the document is shared."""
|
|
242
|
+
return self._shared_doc
|
|
243
|
+
|
|
244
|
+
@shared_doc.setter
|
|
245
|
+
def shared_doc(self, value):
|
|
246
|
+
self._shared_doc = value
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class DocumentProperties:
|
|
250
|
+
"""
|
|
251
|
+
Container for all document-level properties.
|
|
252
|
+
|
|
253
|
+
This includes both core properties (docProps/core.xml) and
|
|
254
|
+
extended properties (docProps/app.xml).
|
|
255
|
+
|
|
256
|
+
Examples:
|
|
257
|
+
>>> wb.document_properties.core.title = "Sales Report"
|
|
258
|
+
>>> wb.document_properties.core.creator = "John Doe"
|
|
259
|
+
>>> wb.document_properties.extended.company = "Acme Corp"
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(self):
|
|
263
|
+
self._core = CoreProperties()
|
|
264
|
+
self._extended = ExtendedProperties()
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def core(self):
|
|
268
|
+
"""Gets core document properties (stored in docProps/core.xml)."""
|
|
269
|
+
return self._core
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def extended(self):
|
|
273
|
+
"""Gets extended/application properties (stored in docProps/app.xml)."""
|
|
274
|
+
return self._extended
|
|
275
|
+
|
|
276
|
+
# Convenience properties that map to core properties
|
|
277
|
+
@property
|
|
278
|
+
def title(self):
|
|
279
|
+
"""Document title."""
|
|
280
|
+
return self._core.title
|
|
281
|
+
|
|
282
|
+
@title.setter
|
|
283
|
+
def title(self, value):
|
|
284
|
+
self._core.title = value
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def subject(self):
|
|
288
|
+
"""Document subject."""
|
|
289
|
+
return self._core.subject
|
|
290
|
+
|
|
291
|
+
@subject.setter
|
|
292
|
+
def subject(self, value):
|
|
293
|
+
self._core.subject = value
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def author(self):
|
|
297
|
+
"""Document author (alias for creator)."""
|
|
298
|
+
return self._core.creator
|
|
299
|
+
|
|
300
|
+
@author.setter
|
|
301
|
+
def author(self, value):
|
|
302
|
+
self._core.creator = value
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def creator(self):
|
|
306
|
+
"""Document creator."""
|
|
307
|
+
return self._core.creator
|
|
308
|
+
|
|
309
|
+
@creator.setter
|
|
310
|
+
def creator(self, value):
|
|
311
|
+
self._core.creator = value
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def keywords(self):
|
|
315
|
+
"""Document keywords."""
|
|
316
|
+
return self._core.keywords
|
|
317
|
+
|
|
318
|
+
@keywords.setter
|
|
319
|
+
def keywords(self, value):
|
|
320
|
+
self._core.keywords = value
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def comments(self):
|
|
324
|
+
"""Document comments (alias for description)."""
|
|
325
|
+
return self._core.description
|
|
326
|
+
|
|
327
|
+
@comments.setter
|
|
328
|
+
def comments(self, value):
|
|
329
|
+
self._core.description = value
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def category(self):
|
|
333
|
+
"""Document category."""
|
|
334
|
+
return self._core.category
|
|
335
|
+
|
|
336
|
+
@category.setter
|
|
337
|
+
def category(self, value):
|
|
338
|
+
self._core.category = value
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def company(self):
|
|
342
|
+
"""Company name."""
|
|
343
|
+
return self._extended.company
|
|
344
|
+
|
|
345
|
+
@company.setter
|
|
346
|
+
def company(self, value):
|
|
347
|
+
self._extended.company = value
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def manager(self):
|
|
351
|
+
"""Manager name."""
|
|
352
|
+
return self._extended.manager
|
|
353
|
+
|
|
354
|
+
@manager.setter
|
|
355
|
+
def manager(self, value):
|
|
356
|
+
self._extended.manager = value
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XLSX Encryption Cryptographic Operations
|
|
3
|
+
|
|
4
|
+
This module implements cryptographic operations for XLSX encryption/decryption
|
|
5
|
+
according to MS-OFFCRYPTO (Agile Encryption).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import struct
|
|
11
|
+
from Crypto.Cipher import AES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PasswordDerivation:
|
|
15
|
+
"""Password derivation helpers for Agile encryption."""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def derive_hash_agile(password, salt, hash_algorithm, spin_count):
|
|
19
|
+
"""
|
|
20
|
+
Derive H_n for Agile encryption (MS-OFFCRYPTO 2.3.4.11).
|
|
21
|
+
|
|
22
|
+
H_0 = Hash(salt + password)
|
|
23
|
+
H_i = Hash(i + H_{i-1}) for i in [0, spin_count)
|
|
24
|
+
"""
|
|
25
|
+
password_bytes = password.encode('utf-16le')
|
|
26
|
+
hash_func_name = hash_algorithm.algorithm_name.lower()
|
|
27
|
+
|
|
28
|
+
h = hashlib.new(hash_func_name)
|
|
29
|
+
h.update(salt)
|
|
30
|
+
h.update(password_bytes)
|
|
31
|
+
current = h.digest()
|
|
32
|
+
|
|
33
|
+
for i in range(spin_count):
|
|
34
|
+
h = hashlib.new(hash_func_name)
|
|
35
|
+
h.update(struct.pack('<I', i))
|
|
36
|
+
h.update(current)
|
|
37
|
+
current = h.digest()
|
|
38
|
+
|
|
39
|
+
return current
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def derive_key_with_block_key(h_base, block_key, hash_algorithm, key_bits):
|
|
43
|
+
"""
|
|
44
|
+
Derive key from H_n and block key (MS-OFFCRYPTO 2.3.4.11).
|
|
45
|
+
"""
|
|
46
|
+
hash_func_name = hash_algorithm.algorithm_name.lower()
|
|
47
|
+
h = hashlib.new(hash_func_name)
|
|
48
|
+
h.update(h_base)
|
|
49
|
+
h.update(block_key)
|
|
50
|
+
derived = h.digest()
|
|
51
|
+
|
|
52
|
+
key_bytes = key_bits // 8
|
|
53
|
+
if len(derived) >= key_bytes:
|
|
54
|
+
return derived[:key_bytes]
|
|
55
|
+
return derived + (b'\x36' * (key_bytes - len(derived)))
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def derive_iv_agile(salt, block_key, hash_algorithm, block_size):
|
|
59
|
+
"""
|
|
60
|
+
Derive IV for Agile encryption (MS-OFFCRYPTO 2.3.4.12).
|
|
61
|
+
|
|
62
|
+
If block_key is None, IV = salt (padded/truncated to block_size).
|
|
63
|
+
Otherwise IV = Hash(salt + block_key), padded/truncated to block_size.
|
|
64
|
+
"""
|
|
65
|
+
if block_key is None:
|
|
66
|
+
iv = salt
|
|
67
|
+
else:
|
|
68
|
+
hash_func_name = hash_algorithm.algorithm_name.lower()
|
|
69
|
+
h = hashlib.new(hash_func_name)
|
|
70
|
+
h.update(salt)
|
|
71
|
+
h.update(block_key)
|
|
72
|
+
iv = h.digest()
|
|
73
|
+
|
|
74
|
+
if len(iv) >= block_size:
|
|
75
|
+
return iv[:block_size]
|
|
76
|
+
return iv + (b'\x36' * (block_size - len(iv)))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EncryptionVerifier:
|
|
80
|
+
"""Encryption verifier generation and validation."""
|
|
81
|
+
|
|
82
|
+
BLOCK_KEY_VERIFIER = bytes.fromhex('fea7d2763b4b9e79')
|
|
83
|
+
BLOCK_KEY_VERIFIER_HASH = bytes.fromhex('d7aa0f6d3061344e')
|
|
84
|
+
BLOCK_KEY_KEYVALUE = bytes.fromhex('146e0be7abacd0d6')
|
|
85
|
+
BLOCK_KEY_DATA_INTEGRITY_KEY = bytes.fromhex('5fb2ad010cb9e1f6')
|
|
86
|
+
BLOCK_KEY_DATA_INTEGRITY_VALUE = bytes.fromhex('a0677f02b22c8433')
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _pad_zero(data, block_size):
|
|
90
|
+
if len(data) % block_size == 0:
|
|
91
|
+
return data
|
|
92
|
+
pad_len = block_size - (len(data) % block_size)
|
|
93
|
+
return data + (b'\x00' * pad_len)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def generate_verifier_agile(password, salt, hash_algorithm, cipher_algorithm, spin_count):
|
|
97
|
+
"""
|
|
98
|
+
Generate PasswordKeyEncryptor fields (MS-OFFCRYPTO 2.3.4.13).
|
|
99
|
+
"""
|
|
100
|
+
verifier_input = os.urandom(16)
|
|
101
|
+
hash_func_name = hash_algorithm.algorithm_name.lower()
|
|
102
|
+
verifier_hash = hashlib.new(hash_func_name, verifier_input).digest()
|
|
103
|
+
|
|
104
|
+
h_base = PasswordDerivation.derive_hash_agile(
|
|
105
|
+
password, salt, hash_algorithm, spin_count
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
verifier_key = PasswordDerivation.derive_key_with_block_key(
|
|
109
|
+
h_base, EncryptionVerifier.BLOCK_KEY_VERIFIER,
|
|
110
|
+
hash_algorithm, cipher_algorithm.key_bits
|
|
111
|
+
)
|
|
112
|
+
iv = PasswordDerivation.derive_iv_agile(
|
|
113
|
+
salt, None, hash_algorithm, cipher_algorithm.block_size
|
|
114
|
+
)
|
|
115
|
+
encrypted_verifier = AES.new(verifier_key, AES.MODE_CBC, iv).encrypt(verifier_input)
|
|
116
|
+
|
|
117
|
+
hash_key = PasswordDerivation.derive_key_with_block_key(
|
|
118
|
+
h_base, EncryptionVerifier.BLOCK_KEY_VERIFIER_HASH,
|
|
119
|
+
hash_algorithm, cipher_algorithm.key_bits
|
|
120
|
+
)
|
|
121
|
+
verifier_hash_padded = EncryptionVerifier._pad_zero(
|
|
122
|
+
verifier_hash, cipher_algorithm.block_size
|
|
123
|
+
)
|
|
124
|
+
encrypted_verifier_hash = AES.new(hash_key, AES.MODE_CBC, iv).encrypt(verifier_hash_padded)
|
|
125
|
+
|
|
126
|
+
key_value = os.urandom(cipher_algorithm.key_bits // 8)
|
|
127
|
+
key_value_key = PasswordDerivation.derive_key_with_block_key(
|
|
128
|
+
h_base, EncryptionVerifier.BLOCK_KEY_KEYVALUE,
|
|
129
|
+
hash_algorithm, cipher_algorithm.key_bits
|
|
130
|
+
)
|
|
131
|
+
encrypted_key_value = AES.new(key_value_key, AES.MODE_CBC, iv).encrypt(key_value)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
'verifier_salt': salt,
|
|
135
|
+
'encrypted_verifier': encrypted_verifier,
|
|
136
|
+
'encrypted_verifier_hash': encrypted_verifier_hash,
|
|
137
|
+
'encrypted_key_value': encrypted_key_value,
|
|
138
|
+
'key_value': key_value,
|
|
139
|
+
'password_hash': h_base
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def verify_password_agile(password, salt, encrypted_verifier, encrypted_verifier_hash,
|
|
144
|
+
hash_algorithm, cipher_algorithm, spin_count):
|
|
145
|
+
"""
|
|
146
|
+
Verify password for Agile encryption (MS-OFFCRYPTO 2.3.4.13).
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
H_n if password is correct, None otherwise.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
h_base = PasswordDerivation.derive_hash_agile(
|
|
153
|
+
password, salt, hash_algorithm, spin_count
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
verifier_key = PasswordDerivation.derive_key_with_block_key(
|
|
157
|
+
h_base, EncryptionVerifier.BLOCK_KEY_VERIFIER,
|
|
158
|
+
hash_algorithm, cipher_algorithm.key_bits
|
|
159
|
+
)
|
|
160
|
+
iv = PasswordDerivation.derive_iv_agile(
|
|
161
|
+
salt, None, hash_algorithm, cipher_algorithm.block_size
|
|
162
|
+
)
|
|
163
|
+
decrypted_verifier = AES.new(verifier_key, AES.MODE_CBC, iv).decrypt(encrypted_verifier)
|
|
164
|
+
|
|
165
|
+
hash_func_name = hash_algorithm.algorithm_name.lower()
|
|
166
|
+
computed_hash = hashlib.new(hash_func_name, decrypted_verifier).digest()
|
|
167
|
+
|
|
168
|
+
hash_key = PasswordDerivation.derive_key_with_block_key(
|
|
169
|
+
h_base, EncryptionVerifier.BLOCK_KEY_VERIFIER_HASH,
|
|
170
|
+
hash_algorithm, cipher_algorithm.key_bits
|
|
171
|
+
)
|
|
172
|
+
decrypted_hash = AES.new(hash_key, AES.MODE_CBC, iv).decrypt(encrypted_verifier_hash)
|
|
173
|
+
|
|
174
|
+
if computed_hash != decrypted_hash[:len(computed_hash)]:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
return h_base
|
|
178
|
+
except Exception:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def decrypt_key_value(h_base, key_salt, encrypted_key_value, hash_algorithm, cipher_algorithm):
|
|
183
|
+
"""Decrypt intermediate key value (MS-OFFCRYPTO 2.3.4.13)."""
|
|
184
|
+
key_value_key = PasswordDerivation.derive_key_with_block_key(
|
|
185
|
+
h_base, EncryptionVerifier.BLOCK_KEY_KEYVALUE,
|
|
186
|
+
hash_algorithm, cipher_algorithm.key_bits
|
|
187
|
+
)
|
|
188
|
+
iv = PasswordDerivation.derive_iv_agile(
|
|
189
|
+
key_salt, None, hash_algorithm, cipher_algorithm.block_size
|
|
190
|
+
)
|
|
191
|
+
return AES.new(key_value_key, AES.MODE_CBC, iv).decrypt(encrypted_key_value)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def decrypt_data_integrity(secret_key, package_salt, encrypted_hmac_key, encrypted_hmac_value,
|
|
195
|
+
hash_algorithm, block_size, hash_size):
|
|
196
|
+
"""Decrypt HMAC key and value (MS-OFFCRYPTO 2.3.4.14)."""
|
|
197
|
+
iv_key = PasswordDerivation.derive_iv_agile(
|
|
198
|
+
package_salt, EncryptionVerifier.BLOCK_KEY_DATA_INTEGRITY_KEY,
|
|
199
|
+
hash_algorithm, block_size
|
|
200
|
+
)
|
|
201
|
+
iv_val = PasswordDerivation.derive_iv_agile(
|
|
202
|
+
package_salt, EncryptionVerifier.BLOCK_KEY_DATA_INTEGRITY_VALUE,
|
|
203
|
+
hash_algorithm, block_size
|
|
204
|
+
)
|
|
205
|
+
hmac_key = AES.new(secret_key, AES.MODE_CBC, iv_key).decrypt(encrypted_hmac_key)
|
|
206
|
+
hmac_value = AES.new(secret_key, AES.MODE_CBC, iv_val).decrypt(encrypted_hmac_value)
|
|
207
|
+
return hmac_key[:hash_size], hmac_value[:hash_size]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class PackageEncryption:
|
|
211
|
+
"""Package data encryption and decryption."""
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def encrypt_package_agile(data, key, package_salt, hash_algorithm, block_size):
|
|
215
|
+
"""Encrypt package data using Agile encryption (MS-OFFCRYPTO 2.3.4.15)."""
|
|
216
|
+
encrypted = bytearray()
|
|
217
|
+
segment_size = 4096
|
|
218
|
+
|
|
219
|
+
for segment_index in range(0, len(data), segment_size):
|
|
220
|
+
chunk = data[segment_index:segment_index + segment_size]
|
|
221
|
+
block_key = struct.pack('<I', segment_index // segment_size)
|
|
222
|
+
iv = PasswordDerivation.derive_iv_agile(
|
|
223
|
+
package_salt, block_key, hash_algorithm, block_size
|
|
224
|
+
)
|
|
225
|
+
if len(chunk) % block_size != 0:
|
|
226
|
+
chunk = chunk + (b'\x00' * (block_size - (len(chunk) % block_size)))
|
|
227
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
228
|
+
encrypted.extend(cipher.encrypt(chunk))
|
|
229
|
+
|
|
230
|
+
return bytes(encrypted)
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def decrypt_package_agile(encrypted_data, key, package_salt, hash_algorithm, block_size):
|
|
234
|
+
"""Decrypt package data using Agile encryption (MS-OFFCRYPTO 2.3.4.15)."""
|
|
235
|
+
decrypted = bytearray()
|
|
236
|
+
segment_size = 4096
|
|
237
|
+
|
|
238
|
+
for segment_index in range(0, len(encrypted_data), segment_size):
|
|
239
|
+
chunk = encrypted_data[segment_index:segment_index + segment_size]
|
|
240
|
+
block_key = struct.pack('<I', segment_index // segment_size)
|
|
241
|
+
iv = PasswordDerivation.derive_iv_agile(
|
|
242
|
+
package_salt, block_key, hash_algorithm, block_size
|
|
243
|
+
)
|
|
244
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
245
|
+
decrypted.extend(cipher.decrypt(chunk))
|
|
246
|
+
|
|
247
|
+
return bytes(decrypted)
|