@cj-tech-master/excelts 8.0.0 → 8.1.0

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 (106) hide show
  1. package/README.md +14 -1
  2. package/README_zh.md +6 -0
  3. package/dist/browser/modules/archive/zip/stream.d.ts +4 -0
  4. package/dist/browser/modules/archive/zip/stream.js +53 -0
  5. package/dist/browser/modules/pdf/core/crypto.d.ts +65 -0
  6. package/dist/browser/modules/pdf/core/crypto.js +637 -0
  7. package/dist/browser/modules/pdf/core/encryption.d.ts +23 -20
  8. package/dist/browser/modules/pdf/core/encryption.js +88 -261
  9. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +6 -4
  10. package/dist/browser/modules/pdf/core/pdf-writer.js +19 -10
  11. package/dist/browser/modules/pdf/index.d.ts +23 -2
  12. package/dist/browser/modules/pdf/index.js +21 -3
  13. package/dist/browser/modules/pdf/reader/annotation-extractor.d.ts +63 -0
  14. package/dist/browser/modules/pdf/reader/annotation-extractor.js +155 -0
  15. package/dist/browser/modules/pdf/reader/cmap-parser.d.ts +70 -0
  16. package/dist/browser/modules/pdf/reader/cmap-parser.js +321 -0
  17. package/dist/browser/modules/pdf/reader/content-interpreter.d.ts +57 -0
  18. package/dist/browser/modules/pdf/reader/content-interpreter.js +715 -0
  19. package/dist/browser/modules/pdf/reader/font-decoder.d.ts +58 -0
  20. package/dist/browser/modules/pdf/reader/font-decoder.js +1513 -0
  21. package/dist/browser/modules/pdf/reader/form-extractor.d.ts +48 -0
  22. package/dist/browser/modules/pdf/reader/form-extractor.js +355 -0
  23. package/dist/browser/modules/pdf/reader/image-extractor.d.ts +55 -0
  24. package/dist/browser/modules/pdf/reader/image-extractor.js +220 -0
  25. package/dist/browser/modules/pdf/reader/metadata-reader.d.ts +56 -0
  26. package/dist/browser/modules/pdf/reader/metadata-reader.js +275 -0
  27. package/dist/browser/modules/pdf/reader/pdf-decrypt.d.ts +26 -0
  28. package/dist/browser/modules/pdf/reader/pdf-decrypt.js +443 -0
  29. package/dist/browser/modules/pdf/reader/pdf-document.d.ts +191 -0
  30. package/dist/browser/modules/pdf/reader/pdf-document.js +818 -0
  31. package/dist/browser/modules/pdf/reader/pdf-parser.d.ts +65 -0
  32. package/dist/browser/modules/pdf/reader/pdf-parser.js +285 -0
  33. package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +143 -0
  34. package/dist/browser/modules/pdf/reader/pdf-reader.js +200 -0
  35. package/dist/browser/modules/pdf/reader/pdf-tokenizer.d.ts +101 -0
  36. package/dist/browser/modules/pdf/reader/pdf-tokenizer.js +543 -0
  37. package/dist/browser/modules/pdf/reader/reader-utils.d.ts +15 -0
  38. package/dist/browser/modules/pdf/reader/reader-utils.js +27 -0
  39. package/dist/browser/modules/pdf/reader/stream-filters.d.ts +20 -0
  40. package/dist/browser/modules/pdf/reader/stream-filters.js +456 -0
  41. package/dist/browser/modules/pdf/reader/text-reconstruction.d.ts +44 -0
  42. package/dist/browser/modules/pdf/reader/text-reconstruction.js +463 -0
  43. package/dist/cjs/modules/archive/zip/stream.js +53 -0
  44. package/dist/cjs/modules/pdf/core/crypto.js +649 -0
  45. package/dist/cjs/modules/pdf/core/encryption.js +88 -263
  46. package/dist/cjs/modules/pdf/core/pdf-writer.js +19 -10
  47. package/dist/cjs/modules/pdf/index.js +23 -4
  48. package/dist/cjs/modules/pdf/reader/annotation-extractor.js +158 -0
  49. package/dist/cjs/modules/pdf/reader/cmap-parser.js +326 -0
  50. package/dist/cjs/modules/pdf/reader/content-interpreter.js +718 -0
  51. package/dist/cjs/modules/pdf/reader/font-decoder.js +1518 -0
  52. package/dist/cjs/modules/pdf/reader/form-extractor.js +358 -0
  53. package/dist/cjs/modules/pdf/reader/image-extractor.js +223 -0
  54. package/dist/cjs/modules/pdf/reader/metadata-reader.js +278 -0
  55. package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +447 -0
  56. package/dist/cjs/modules/pdf/reader/pdf-document.js +822 -0
  57. package/dist/cjs/modules/pdf/reader/pdf-parser.js +301 -0
  58. package/dist/cjs/modules/pdf/reader/pdf-reader.js +203 -0
  59. package/dist/cjs/modules/pdf/reader/pdf-tokenizer.js +517 -0
  60. package/dist/cjs/modules/pdf/reader/reader-utils.js +30 -0
  61. package/dist/cjs/modules/pdf/reader/stream-filters.js +459 -0
  62. package/dist/cjs/modules/pdf/reader/text-reconstruction.js +467 -0
  63. package/dist/esm/modules/archive/zip/stream.js +53 -0
  64. package/dist/esm/modules/pdf/core/crypto.js +637 -0
  65. package/dist/esm/modules/pdf/core/encryption.js +88 -261
  66. package/dist/esm/modules/pdf/core/pdf-writer.js +19 -10
  67. package/dist/esm/modules/pdf/index.js +21 -3
  68. package/dist/esm/modules/pdf/reader/annotation-extractor.js +155 -0
  69. package/dist/esm/modules/pdf/reader/cmap-parser.js +321 -0
  70. package/dist/esm/modules/pdf/reader/content-interpreter.js +715 -0
  71. package/dist/esm/modules/pdf/reader/font-decoder.js +1513 -0
  72. package/dist/esm/modules/pdf/reader/form-extractor.js +355 -0
  73. package/dist/esm/modules/pdf/reader/image-extractor.js +220 -0
  74. package/dist/esm/modules/pdf/reader/metadata-reader.js +275 -0
  75. package/dist/esm/modules/pdf/reader/pdf-decrypt.js +443 -0
  76. package/dist/esm/modules/pdf/reader/pdf-document.js +818 -0
  77. package/dist/esm/modules/pdf/reader/pdf-parser.js +285 -0
  78. package/dist/esm/modules/pdf/reader/pdf-reader.js +200 -0
  79. package/dist/esm/modules/pdf/reader/pdf-tokenizer.js +543 -0
  80. package/dist/esm/modules/pdf/reader/reader-utils.js +27 -0
  81. package/dist/esm/modules/pdf/reader/stream-filters.js +456 -0
  82. package/dist/esm/modules/pdf/reader/text-reconstruction.js +463 -0
  83. package/dist/iife/excelts.iife.js +703 -267
  84. package/dist/iife/excelts.iife.js.map +1 -1
  85. package/dist/iife/excelts.iife.min.js +35 -35
  86. package/dist/types/modules/archive/zip/stream.d.ts +4 -0
  87. package/dist/types/modules/pdf/core/crypto.d.ts +65 -0
  88. package/dist/types/modules/pdf/core/encryption.d.ts +23 -20
  89. package/dist/types/modules/pdf/core/pdf-writer.d.ts +6 -4
  90. package/dist/types/modules/pdf/index.d.ts +23 -2
  91. package/dist/types/modules/pdf/reader/annotation-extractor.d.ts +63 -0
  92. package/dist/types/modules/pdf/reader/cmap-parser.d.ts +70 -0
  93. package/dist/types/modules/pdf/reader/content-interpreter.d.ts +57 -0
  94. package/dist/types/modules/pdf/reader/font-decoder.d.ts +58 -0
  95. package/dist/types/modules/pdf/reader/form-extractor.d.ts +48 -0
  96. package/dist/types/modules/pdf/reader/image-extractor.d.ts +55 -0
  97. package/dist/types/modules/pdf/reader/metadata-reader.d.ts +56 -0
  98. package/dist/types/modules/pdf/reader/pdf-decrypt.d.ts +26 -0
  99. package/dist/types/modules/pdf/reader/pdf-document.d.ts +191 -0
  100. package/dist/types/modules/pdf/reader/pdf-parser.d.ts +65 -0
  101. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +143 -0
  102. package/dist/types/modules/pdf/reader/pdf-tokenizer.d.ts +101 -0
  103. package/dist/types/modules/pdf/reader/reader-utils.d.ts +15 -0
  104. package/dist/types/modules/pdf/reader/stream-filters.d.ts +20 -0
  105. package/dist/types/modules/pdf/reader/text-reconstruction.d.ts +44 -0
  106. package/package.json +1 -1
@@ -0,0 +1,447 @@
1
+ "use strict";
2
+ /**
3
+ * PDF decryption for reading encrypted PDFs.
4
+ *
5
+ * Supports:
6
+ * - Standard Security Handler (V1/V2/V4/V5, R2/R3/R4/R5)
7
+ * - RC4 encryption (40-bit and 128-bit)
8
+ * - AES-128 encryption (PDF 1.6+)
9
+ * - AES-256 encryption (PDF 2.0, V=5, R=5)
10
+ *
11
+ * @see PDF Reference 1.7, §3.5 - Encryption
12
+ * @see PDF 2.0 (ISO 32000-2), §7.6 - Encryption
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.initDecryption = initDecryption;
16
+ exports.isEncrypted = isEncrypted;
17
+ const crypto_1 = require("../core/crypto");
18
+ const pdf_parser_1 = require("./pdf-parser");
19
+ const errors_1 = require("../errors");
20
+ // =============================================================================
21
+ // Constants
22
+ // =============================================================================
23
+ /** PDF password padding string (32 bytes) per PDF spec §3.5.2 */
24
+ const PASSWORD_PADDING = new Uint8Array([
25
+ 0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08,
26
+ 0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a
27
+ ]);
28
+ /** Cached TextEncoder instance */
29
+ const textEncoder = new TextEncoder();
30
+ // =============================================================================
31
+ // Public API
32
+ // =============================================================================
33
+ /**
34
+ * Initialize decryption for a PDF document.
35
+ * Returns true if decryption was successfully initialized, false if
36
+ * the password was incorrect.
37
+ *
38
+ * @param doc - The PDF document
39
+ * @param password - User or owner password (empty string for no password)
40
+ */
41
+ function initDecryption(doc, password = "") {
42
+ const encryptDict = doc.derefDict(doc.trailer.get("Encrypt"));
43
+ if (!encryptDict) {
44
+ return true; // Not encrypted
45
+ }
46
+ const filter = (0, pdf_parser_1.dictGetName)(encryptDict, "Filter");
47
+ if (filter !== "Standard") {
48
+ throw new errors_1.PdfStructureError(`Unsupported encryption filter: ${filter}`);
49
+ }
50
+ const v = (0, pdf_parser_1.dictGetNumber)(encryptDict, "V") ?? 0;
51
+ const r = (0, pdf_parser_1.dictGetNumber)(encryptDict, "R") ?? 0;
52
+ const keyLength = ((0, pdf_parser_1.dictGetNumber)(encryptDict, "Length") ?? 40) / 8; // bits → bytes
53
+ const permissions = (0, pdf_parser_1.dictGetNumber)(encryptDict, "P") ?? 0;
54
+ const oValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "O");
55
+ const uValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "U");
56
+ if (!oValue || !uValue) {
57
+ throw new errors_1.PdfStructureError("Missing /O or /U values in Encrypt dictionary");
58
+ }
59
+ // Get file ID from trailer
60
+ const idArray = (0, pdf_parser_1.dictGetArray)(doc.trailer, "ID");
61
+ const fileId = idArray && idArray.length > 0 && idArray[0] instanceof Uint8Array
62
+ ? idArray[0]
63
+ : new Uint8Array(0);
64
+ // Determine EncryptMetadata flag (default true per spec)
65
+ const encryptMetadata = readEncryptMetadata(encryptDict);
66
+ // Handle V=5 (AES-256, PDF 2.0)
67
+ if (v === 5) {
68
+ return initDecryptionV5(doc, encryptDict, password, r, oValue, uValue, permissions, fileId);
69
+ }
70
+ // Determine if we should use AES
71
+ const useAes = v === 4 && isAesCryptFilter(encryptDict);
72
+ // Try user password first, then owner password
73
+ let encryptionKey = tryUserPassword(password, oValue, permissions, fileId, r, keyLength, uValue, encryptMetadata);
74
+ if (!encryptionKey) {
75
+ // Try as owner password
76
+ const derivedUser = deriveUserPasswordFromOwner(password, oValue, r, keyLength);
77
+ encryptionKey = tryUserPassword(derivedUser, oValue, permissions, fileId, r, keyLength, uValue, encryptMetadata);
78
+ }
79
+ if (!encryptionKey) {
80
+ // Try empty password
81
+ if (password !== "") {
82
+ encryptionKey = tryUserPassword("", oValue, permissions, fileId, r, keyLength, uValue, encryptMetadata);
83
+ }
84
+ }
85
+ if (!encryptionKey) {
86
+ return false; // Password incorrect
87
+ }
88
+ // Set up decryption function
89
+ const finalKey = encryptionKey;
90
+ if (useAes) {
91
+ doc.decryptFn = (data, objNum, gen) => decryptAes128(data, objNum, gen, finalKey);
92
+ }
93
+ else {
94
+ doc.decryptFn = (data, objNum, gen) => decryptRc4PerObject(data, objNum, gen, finalKey);
95
+ }
96
+ return true;
97
+ }
98
+ /**
99
+ * Check if the document is encrypted.
100
+ */
101
+ function isEncrypted(doc) {
102
+ return doc.trailer.has("Encrypt");
103
+ }
104
+ // =============================================================================
105
+ // V5 (AES-256) Decryption
106
+ // =============================================================================
107
+ /**
108
+ * Initialize decryption for V=5 (AES-256, PDF 2.0).
109
+ * Supports R=5 using SHA-256 based key derivation (Algorithm 2.A).
110
+ */
111
+ function initDecryptionV5(doc, encryptDict, password, revision, oValue, uValue, _permissions, _fileId) {
112
+ if (revision === 6) {
113
+ throw new errors_1.PdfStructureError("R=6 (PDF 2.0 extension) requires SHA-384/SHA-512 which is not yet supported");
114
+ }
115
+ if (revision !== 5) {
116
+ throw new errors_1.PdfStructureError(`Unsupported revision ${revision} for V=5 encryption`);
117
+ }
118
+ const oeValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "OE");
119
+ const ueValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "UE");
120
+ if (!oeValue || !ueValue) {
121
+ throw new errors_1.PdfStructureError("Missing /OE or /UE values in V=5 Encrypt dictionary");
122
+ }
123
+ // O value layout: 32 bytes hash + 8 bytes validation salt + 8 bytes key salt
124
+ // U value layout: 32 bytes hash + 8 bytes validation salt + 8 bytes key salt
125
+ if (oValue.length < 48 || uValue.length < 48) {
126
+ throw new errors_1.PdfStructureError("Invalid /O or /U length for V=5 encryption");
127
+ }
128
+ const passwordBytes = truncatePassword(password);
129
+ // Try user password (Algorithm 2.A step a - user)
130
+ let encryptionKey = tryUserPasswordV5(passwordBytes, uValue, ueValue);
131
+ if (!encryptionKey) {
132
+ // Try owner password (Algorithm 2.A step a - owner)
133
+ encryptionKey = tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue);
134
+ }
135
+ if (!encryptionKey) {
136
+ // Try empty password
137
+ if (password !== "") {
138
+ const emptyBytes = new Uint8Array(0);
139
+ encryptionKey = tryUserPasswordV5(emptyBytes, uValue, ueValue);
140
+ if (!encryptionKey) {
141
+ encryptionKey = tryOwnerPasswordV5(emptyBytes, oValue, oeValue, uValue);
142
+ }
143
+ }
144
+ }
145
+ if (!encryptionKey) {
146
+ return false;
147
+ }
148
+ // V=5 always uses AES-256 with the file encryption key directly (no per-object key derivation)
149
+ const finalKey = encryptionKey;
150
+ doc.decryptFn = (data, _objNum, _gen) => decryptAes256Direct(data, finalKey);
151
+ return true;
152
+ }
153
+ /**
154
+ * Truncate password to 127 bytes (UTF-8) per PDF 2.0 spec.
155
+ */
156
+ function truncatePassword(password) {
157
+ const bytes = textEncoder.encode(password);
158
+ return bytes.length > 127 ? bytes.subarray(0, 127) : bytes;
159
+ }
160
+ /**
161
+ * Try user password for V=5/R=5.
162
+ * Validates using SHA-256(password + validation salt from U).
163
+ * If valid, derives file encryption key using SHA-256(password + key salt from U).
164
+ */
165
+ function tryUserPasswordV5(passwordBytes, uValue, ueValue) {
166
+ // U = hash(32) + validation salt(8) + key salt(8)
167
+ const uHash = uValue.subarray(0, 32);
168
+ const uValidationSalt = uValue.subarray(32, 40);
169
+ const uKeySalt = uValue.subarray(40, 48);
170
+ // Validate: SHA-256(password + validation salt) == first 32 bytes of U
171
+ const validateInput = (0, crypto_1.concatArrays)(passwordBytes, uValidationSalt);
172
+ const computedHash = (0, crypto_1.sha256)(validateInput);
173
+ if (!arraysEqual(computedHash, uHash)) {
174
+ return null;
175
+ }
176
+ // Derive key: SHA-256(password + key salt) => use as AES-256 key to decrypt UE
177
+ const keyInput = (0, crypto_1.concatArrays)(passwordBytes, uKeySalt);
178
+ const keyHash = (0, crypto_1.sha256)(keyInput);
179
+ // Decrypt UE with this key using AES-256-CBC with zero IV
180
+ const zeroIv = new Uint8Array(16);
181
+ return (0, crypto_1.aesCbcDecryptRaw)(ueValue.subarray(0, 32), keyHash, zeroIv);
182
+ }
183
+ /**
184
+ * Try owner password for V=5/R=5.
185
+ * Validates using SHA-256(password + validation salt from O + U(48)).
186
+ * If valid, derives file encryption key using SHA-256(password + key salt from O + U(48)).
187
+ */
188
+ function tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue) {
189
+ // O = hash(32) + validation salt(8) + key salt(8)
190
+ const oHash = oValue.subarray(0, 32);
191
+ const oValidationSalt = oValue.subarray(32, 40);
192
+ const oKeySalt = oValue.subarray(40, 48);
193
+ const u48 = uValue.subarray(0, 48);
194
+ // Validate: SHA-256(password + validation salt + U(0..47)) == first 32 bytes of O
195
+ const validateInput = (0, crypto_1.concatArrays)(passwordBytes, oValidationSalt, u48);
196
+ const computedHash = (0, crypto_1.sha256)(validateInput);
197
+ if (!arraysEqual(computedHash, oHash)) {
198
+ return null;
199
+ }
200
+ // Derive key: SHA-256(password + key salt + U(0..47))
201
+ const keyInput = (0, crypto_1.concatArrays)(passwordBytes, oKeySalt, u48);
202
+ const keyHash = (0, crypto_1.sha256)(keyInput);
203
+ // Decrypt OE with this key using AES-256-CBC with zero IV
204
+ const zeroIv = new Uint8Array(16);
205
+ return (0, crypto_1.aesCbcDecryptRaw)(oeValue.subarray(0, 32), keyHash, zeroIv);
206
+ }
207
+ /**
208
+ * Decrypt data using AES-256 directly (no per-object key derivation).
209
+ * For V=5, the file encryption key is used directly. The first 16 bytes are IV.
210
+ */
211
+ function decryptAes256Direct(data, encryptionKey) {
212
+ if (data.length < 16) {
213
+ return data;
214
+ }
215
+ const iv = data.subarray(0, 16);
216
+ const ciphertext = data.subarray(16);
217
+ if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
218
+ return data;
219
+ }
220
+ return (0, crypto_1.aesCbcDecrypt)(ciphertext, encryptionKey, iv);
221
+ }
222
+ // =============================================================================
223
+ // Password Verification (V1-V4)
224
+ // =============================================================================
225
+ /**
226
+ * Try to authenticate with the user password.
227
+ * Returns the encryption key if successful, null otherwise.
228
+ */
229
+ function tryUserPassword(password, oValue, permissions, fileId, revision, keyLength, uValue, encryptMetadata) {
230
+ const key = computeEncryptionKeyForReading(password, oValue, permissions, fileId, revision, keyLength, encryptMetadata);
231
+ // Verify against U value
232
+ if (revision === 2) {
233
+ // R2: encrypt password padding with key, compare to U
234
+ const encrypted = (0, crypto_1.rc4)(key, PASSWORD_PADDING);
235
+ if (arraysEqual(encrypted, uValue.subarray(0, 32))) {
236
+ return key;
237
+ }
238
+ }
239
+ else if (revision >= 3) {
240
+ // R3/R4: MD5(padding + fileId), encrypt, iterate 19 times
241
+ const hashInput = new Uint8Array(32 + fileId.length);
242
+ hashInput.set(PASSWORD_PADDING);
243
+ hashInput.set(fileId, 32);
244
+ const hash = (0, crypto_1.md5)(hashInput);
245
+ let result = (0, crypto_1.rc4)(key, hash);
246
+ for (let i = 1; i <= 19; i++) {
247
+ const modKey = new Uint8Array(key.length);
248
+ for (let j = 0; j < key.length; j++) {
249
+ modKey[j] = key[j] ^ i;
250
+ }
251
+ result = (0, crypto_1.rc4)(modKey, result);
252
+ }
253
+ // Compare first 16 bytes
254
+ if (arraysEqual(result.subarray(0, 16), uValue.subarray(0, 16))) {
255
+ return key;
256
+ }
257
+ }
258
+ return null;
259
+ }
260
+ /**
261
+ * Compute the encryption key for reading (Algorithm 2, PDF spec §3.5.2).
262
+ */
263
+ function computeEncryptionKeyForReading(password, oValue, permissions, fileId, revision, keyLength, encryptMetadata) {
264
+ const paddedPwd = padPassword(password);
265
+ // When encryptMetadata is false and revision >= 4, append 4 bytes of 0xFF
266
+ const extraBytes = revision >= 4 && !encryptMetadata ? 4 : 0;
267
+ const input = new Uint8Array(32 + 32 + 4 + fileId.length + extraBytes);
268
+ let offset = 0;
269
+ input.set(paddedPwd, offset);
270
+ offset += 32;
271
+ input.set(oValue.subarray(0, 32), offset);
272
+ offset += 32;
273
+ // P value as 4 LE bytes
274
+ input[offset] = permissions & 0xff;
275
+ input[offset + 1] = (permissions >> 8) & 0xff;
276
+ input[offset + 2] = (permissions >> 16) & 0xff;
277
+ input[offset + 3] = (permissions >> 24) & 0xff;
278
+ offset += 4;
279
+ input.set(fileId, offset);
280
+ offset += fileId.length;
281
+ // If EncryptMetadata is false and revision >= 4, append 0xFFFFFFFF
282
+ if (revision >= 4 && !encryptMetadata) {
283
+ input[offset] = 0xff;
284
+ input[offset + 1] = 0xff;
285
+ input[offset + 2] = 0xff;
286
+ input[offset + 3] = 0xff;
287
+ offset += 4;
288
+ }
289
+ let hash = (0, crypto_1.md5)(input.subarray(0, offset));
290
+ // For revision >= 3, hash 50 more times
291
+ if (revision >= 3) {
292
+ for (let i = 0; i < 50; i++) {
293
+ hash = (0, crypto_1.md5)(hash.subarray(0, keyLength));
294
+ }
295
+ }
296
+ return hash.subarray(0, keyLength);
297
+ }
298
+ /**
299
+ * Derive the user password from the owner password.
300
+ * Uses Algorithm 7 from PDF spec §3.5.2.
301
+ */
302
+ function deriveUserPasswordFromOwner(ownerPassword, oValue, revision, keyLength) {
303
+ let hash = (0, crypto_1.md5)(padPassword(ownerPassword));
304
+ if (revision >= 3) {
305
+ for (let i = 0; i < 50; i++) {
306
+ hash = (0, crypto_1.md5)(hash.subarray(0, keyLength));
307
+ }
308
+ }
309
+ const key = hash.subarray(0, keyLength);
310
+ let result = new Uint8Array(oValue.subarray(0, 32));
311
+ if (revision === 2) {
312
+ result = (0, crypto_1.rc4)(key, result);
313
+ }
314
+ else if (revision >= 3) {
315
+ for (let i = 19; i >= 0; i--) {
316
+ const modKey = new Uint8Array(key.length);
317
+ for (let j = 0; j < key.length; j++) {
318
+ modKey[j] = key[j] ^ i;
319
+ }
320
+ result = (0, crypto_1.rc4)(modKey, result);
321
+ }
322
+ }
323
+ // Convert result bytes to password string
324
+ let pwd = "";
325
+ for (let i = 0; i < 32; i++) {
326
+ if (result[i] === PASSWORD_PADDING[0] &&
327
+ arraysEqual(result.subarray(i, i + Math.min(32 - i, 32)), PASSWORD_PADDING.subarray(0, Math.min(32 - i, 32)))) {
328
+ break;
329
+ }
330
+ pwd += String.fromCharCode(result[i]);
331
+ }
332
+ return pwd;
333
+ }
334
+ // =============================================================================
335
+ // AES-128 Decryption
336
+ // =============================================================================
337
+ /**
338
+ * Decrypt data using RC4 with per-object key derivation.
339
+ * Per-object key = MD5(encryptionKey + objNum(3LE) + genNum(2LE)), truncated to min(n+5, 16).
340
+ */
341
+ function decryptRc4PerObject(data, objectNumber, generation, encryptionKey) {
342
+ const keyInput = new Uint8Array(encryptionKey.length + 5);
343
+ keyInput.set(encryptionKey);
344
+ keyInput[encryptionKey.length] = objectNumber & 0xff;
345
+ keyInput[encryptionKey.length + 1] = (objectNumber >> 8) & 0xff;
346
+ keyInput[encryptionKey.length + 2] = (objectNumber >> 16) & 0xff;
347
+ keyInput[encryptionKey.length + 3] = generation & 0xff;
348
+ keyInput[encryptionKey.length + 4] = (generation >> 8) & 0xff;
349
+ const objKey = (0, crypto_1.md5)(keyInput);
350
+ const keyLen = Math.min(encryptionKey.length + 5, 16);
351
+ return (0, crypto_1.rc4)(objKey.subarray(0, keyLen), data);
352
+ }
353
+ /**
354
+ * Decrypt data using AES-128-CBC.
355
+ * Per PDF spec, the first 16 bytes of the data are the IV.
356
+ */
357
+ function decryptAes128(data, objectNumber, generation, encryptionKey) {
358
+ if (data.length < 16) {
359
+ return data;
360
+ }
361
+ // Compute per-object key: MD5(encryptionKey + objNum(3LE) + genNum(2LE) + "sAlT")
362
+ const keyInput = new Uint8Array(encryptionKey.length + 5 + 4);
363
+ keyInput.set(encryptionKey);
364
+ keyInput[encryptionKey.length] = objectNumber & 0xff;
365
+ keyInput[encryptionKey.length + 1] = (objectNumber >> 8) & 0xff;
366
+ keyInput[encryptionKey.length + 2] = (objectNumber >> 16) & 0xff;
367
+ keyInput[encryptionKey.length + 3] = generation & 0xff;
368
+ keyInput[encryptionKey.length + 4] = (generation >> 8) & 0xff;
369
+ // AES salt
370
+ keyInput[encryptionKey.length + 5] = 0x73; // s
371
+ keyInput[encryptionKey.length + 6] = 0x41; // A
372
+ keyInput[encryptionKey.length + 7] = 0x6c; // l
373
+ keyInput[encryptionKey.length + 8] = 0x54; // T
374
+ const objKey = (0, crypto_1.md5)(keyInput);
375
+ const keyLen = Math.min(encryptionKey.length + 5, 16);
376
+ const aesKey = objKey.subarray(0, keyLen);
377
+ // Extract IV (first 16 bytes) and ciphertext
378
+ const iv = data.subarray(0, 16);
379
+ const ciphertext = data.subarray(16);
380
+ if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
381
+ return data;
382
+ }
383
+ return (0, crypto_1.aesCbcDecrypt)(ciphertext, aesKey, iv);
384
+ }
385
+ // =============================================================================
386
+ // Helpers
387
+ // =============================================================================
388
+ function padPassword(password) {
389
+ const result = new Uint8Array(32);
390
+ const bytes = textEncoder.encode(password);
391
+ const len = Math.min(bytes.length, 32);
392
+ result.set(bytes.subarray(0, len));
393
+ result.set(PASSWORD_PADDING.subarray(0, 32 - len), len);
394
+ return result;
395
+ }
396
+ function arraysEqual(a, b) {
397
+ if (a.length !== b.length) {
398
+ return false;
399
+ }
400
+ for (let i = 0; i < a.length; i++) {
401
+ if (a[i] !== b[i]) {
402
+ return false;
403
+ }
404
+ }
405
+ return true;
406
+ }
407
+ /**
408
+ * Read the EncryptMetadata flag from the encrypt dictionary.
409
+ * Per spec, defaults to true if not present.
410
+ * Checks both the top-level dict and CF/StdCF sub-dictionary.
411
+ */
412
+ function readEncryptMetadata(encryptDict) {
413
+ // Check top-level EncryptMetadata first
414
+ const topLevel = (0, pdf_parser_1.dictGetBool)(encryptDict, "EncryptMetadata");
415
+ if (topLevel !== undefined) {
416
+ return topLevel;
417
+ }
418
+ // Check CF/StdCF/EncryptMetadata
419
+ const cf = encryptDict.get("CF");
420
+ if (cf && cf instanceof Map) {
421
+ const stdCF = cf.get("StdCF");
422
+ if (stdCF && stdCF instanceof Map) {
423
+ const cfVal = stdCF.get("EncryptMetadata");
424
+ if (typeof cfVal === "boolean") {
425
+ return cfVal;
426
+ }
427
+ }
428
+ }
429
+ // Default per spec
430
+ return true;
431
+ }
432
+ /**
433
+ * Check if V4 encryption uses AES (vs RC4).
434
+ */
435
+ function isAesCryptFilter(encryptDict) {
436
+ const cf = encryptDict.get("CF");
437
+ if (!cf || !(cf instanceof Map)) {
438
+ return false;
439
+ }
440
+ // Check StdCF filter
441
+ const stdCF = cf.get("StdCF");
442
+ if (!stdCF || !(stdCF instanceof Map)) {
443
+ return false;
444
+ }
445
+ const cfm = stdCF.get("CFM");
446
+ return cfm === "AESV2";
447
+ }