documonster 0.19.0 → 0.20.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 (160) hide show
  1. package/README.md +1 -1
  2. package/README_zh.md +1 -1
  3. package/dist/esm/modules/archive/compression/compress.base.js +20 -6
  4. package/dist/esm/modules/archive/compression/compress.browser.js +45 -32
  5. package/dist/esm/modules/archive/compression/compress.js +74 -18
  6. package/dist/esm/modules/archive/compression/deflate-fallback.js +14 -3
  7. package/dist/esm/modules/archive/compression/worker-pool/pool.browser.js +9 -3
  8. package/dist/esm/modules/archive/compression/worker-pool/worker-script.js +35 -7
  9. package/dist/esm/modules/archive/core/errors.js +26 -4
  10. package/dist/esm/modules/archive/index.base.js +1 -1
  11. package/dist/esm/modules/archive/unzip/zip-extract-core.js +74 -34
  12. package/dist/esm/modules/archive/unzip/zip-parser.js +55 -9
  13. package/dist/esm/modules/archive/zip-spec/zip-parser-core.js +14 -2
  14. package/dist/esm/modules/csv/parse/scanner/scanner.js +3 -0
  15. package/dist/esm/modules/csv/parse/sync.js +3 -2
  16. package/dist/esm/modules/csv/stream/parser.js +40 -4
  17. package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
  18. package/dist/esm/modules/excel/core/cell.js +22 -2
  19. package/dist/esm/modules/excel/core/table.js +39 -2
  20. package/dist/esm/modules/excel/core/worksheet-core.js +39 -0
  21. package/dist/esm/modules/excel/core/worksheet-load.js +3 -1
  22. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +34 -11
  23. package/dist/esm/modules/excel/surface/cell.js +64 -23
  24. package/dist/esm/modules/excel/utils/cell-format.js +3 -2
  25. package/dist/esm/modules/excel/xlsb/read/package.js +3 -1
  26. package/dist/esm/modules/excel/xlsx/read/package.js +17 -1
  27. package/dist/esm/modules/pdf/builder/document-builder.js +3 -1
  28. package/dist/esm/modules/pdf/builder/pdf-editor.js +218 -58
  29. package/dist/esm/modules/pdf/core/pdf-kdf.browser.js +9 -1
  30. package/dist/esm/modules/pdf/core/pdf-kdf.js +8 -0
  31. package/dist/esm/modules/pdf/core/pdf-writer.js +159 -149
  32. package/dist/esm/modules/pdf/core/pdfa.js +7 -3
  33. package/dist/esm/modules/pdf/errors.js +25 -0
  34. package/dist/esm/modules/pdf/index.js +1 -1
  35. package/dist/esm/modules/pdf/reader/pdf-decrypt.js +136 -74
  36. package/dist/esm/modules/pdf/reader/pdf-document.js +314 -105
  37. package/dist/esm/modules/pdf/reader/pdf-parser.js +29 -11
  38. package/dist/esm/modules/pdf/reader/pdf-reader.js +8 -1
  39. package/dist/esm/modules/pdf/reader/stream-filters.js +139 -77
  40. package/dist/esm/modules/pdf/render/pdf-exporter.js +3 -1
  41. package/dist/esm/modules/pdf/word-bridge.js +22 -48
  42. package/dist/esm/modules/word/advanced/field-engine.js +75 -56
  43. package/dist/esm/modules/word/builder/document-handle.js +15 -1
  44. package/dist/esm/modules/word/convert/docx-to-semantic.js +84 -83
  45. package/dist/esm/modules/word/convert/html/html-renderer.js +107 -78
  46. package/dist/esm/modules/word/convert/markdown/markdown-import.js +43 -10
  47. package/dist/esm/modules/word/convert/markdown/markdown-renderer.js +98 -130
  48. package/dist/esm/modules/word/convert/odt/odt.js +149 -55
  49. package/dist/esm/modules/word/core/text-utils.js +9 -0
  50. package/dist/esm/modules/word/errors.js +1 -1
  51. package/dist/esm/modules/word/layout/layout-constants.js +20 -23
  52. package/dist/esm/modules/word/layout/layout-full.js +259 -280
  53. package/dist/esm/modules/word/layout/layout.js +140 -197
  54. package/dist/esm/modules/word/query/final-view.js +49 -0
  55. package/dist/esm/modules/word/query/heading.js +81 -0
  56. package/dist/esm/modules/word/query/merge.js +39 -22
  57. package/dist/esm/modules/word/query/numbering-counter.js +108 -0
  58. package/dist/esm/modules/word/query/page-field.js +212 -0
  59. package/dist/esm/modules/word/query/search.js +15 -14
  60. package/dist/esm/modules/word/query/split.js +15 -25
  61. package/dist/esm/modules/word/query/style-resolve.js +196 -53
  62. package/dist/esm/modules/word/query/style-resolver.js +24 -0
  63. package/dist/esm/modules/word/reader/chart-parser.js +17 -7
  64. package/dist/esm/modules/word/reader/comments-parser.js +4 -4
  65. package/dist/esm/modules/word/reader/doc-props-parsers.js +10 -9
  66. package/dist/esm/modules/word/reader/docx-reader.js +46 -32
  67. package/dist/esm/modules/word/reader/metadata-parsers.js +5 -5
  68. package/dist/esm/modules/word/reader/numbering-parser.js +4 -3
  69. package/dist/esm/modules/word/reader/part-xml.js +26 -0
  70. package/dist/esm/modules/word/reader/reader-context.js +3 -3
  71. package/dist/esm/modules/word/reader/settings-parser.js +3 -3
  72. package/dist/esm/modules/word/reader/styles-parser.js +3 -3
  73. package/dist/esm/modules/word/reader/theme-parser.js +4 -3
  74. package/dist/esm/modules/word/surface/query.js +1 -1
  75. package/dist/esm/modules/word/writer/docx-packager.js +4 -1
  76. package/dist/esm/modules/word/writer/streaming-writer.js +11 -2
  77. package/dist/esm/modules/xml/dom.js +4 -8
  78. package/dist/esm/modules/xml/errors.js +1 -3
  79. package/dist/esm/modules/xml/sax.js +8 -7
  80. package/dist/esm/modules/xml/to-object.js +4 -8
  81. package/dist/esm/utils/crypto.browser.js +147 -0
  82. package/dist/esm/utils/crypto.js +14 -0
  83. package/dist/esm/utils/limits.js +19 -0
  84. package/dist/iife/documonster.archive.iife.min.js +39 -11
  85. package/dist/iife/documonster.csv.iife.min.js +17 -17
  86. package/dist/iife/documonster.draw.iife.min.js +1 -1
  87. package/dist/iife/documonster.excel.iife.min.js +88 -60
  88. package/dist/iife/documonster.formula.iife.min.js +1 -1
  89. package/dist/iife/documonster.markdown.iife.min.js +1 -1
  90. package/dist/iife/documonster.mermaid.iife.min.js +1 -1
  91. package/dist/iife/documonster.pdf.iife.min.js +44 -42
  92. package/dist/iife/documonster.stream.iife.min.js +1 -1
  93. package/dist/iife/documonster.word.iife.min.js +95 -67
  94. package/dist/iife/documonster.xml.iife.min.js +6 -6
  95. package/dist/types/modules/archive/compression/compress.base.d.ts +12 -3
  96. package/dist/types/modules/archive/compression/compress.browser.d.ts +7 -4
  97. package/dist/types/modules/archive/compression/compress.d.ts +9 -4
  98. package/dist/types/modules/archive/compression/deflate-fallback.d.ts +3 -1
  99. package/dist/types/modules/archive/compression/worker-pool/pool.browser.d.ts +4 -1
  100. package/dist/types/modules/archive/compression/worker-pool/types.d.ts +4 -0
  101. package/dist/types/modules/archive/core/errors.d.ts +31 -2
  102. package/dist/types/modules/archive/index.base.d.ts +1 -1
  103. package/dist/types/modules/archive/unzip/zip-extract-core.d.ts +9 -2
  104. package/dist/types/modules/archive/unzip/zip-parser.d.ts +33 -0
  105. package/dist/types/modules/archive/zip-spec/zip-parser-core.d.ts +6 -0
  106. package/dist/types/modules/csv/parse/scanner/types.d.ts +5 -0
  107. package/dist/types/modules/csv/stream/parser.d.ts +16 -0
  108. package/dist/types/modules/csv/worker/worker-script.generated.d.ts +1 -1
  109. package/dist/types/modules/excel/core/table.d.ts +15 -0
  110. package/dist/types/modules/excel/core/worksheet-core.d.ts +26 -0
  111. package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +11 -0
  112. package/dist/types/modules/excel/surface/cell.d.ts +8 -8
  113. package/dist/types/modules/excel/xlsx/types.d.ts +16 -0
  114. package/dist/types/modules/pdf/builder/document-builder.d.ts +7 -0
  115. package/dist/types/modules/pdf/builder/pdf-editor.d.ts +49 -5
  116. package/dist/types/modules/pdf/core/pdf-kdf.browser.d.ts +4 -0
  117. package/dist/types/modules/pdf/core/pdf-kdf.d.ts +4 -0
  118. package/dist/types/modules/pdf/core/pdf-writer.d.ts +49 -10
  119. package/dist/types/modules/pdf/core/pdfa.d.ts +2 -0
  120. package/dist/types/modules/pdf/errors.d.ts +19 -0
  121. package/dist/types/modules/pdf/index.d.ts +2 -2
  122. package/dist/types/modules/pdf/reader/pdf-document.d.ts +83 -5
  123. package/dist/types/modules/pdf/reader/pdf-parser.d.ts +2 -2
  124. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +7 -0
  125. package/dist/types/modules/pdf/reader/stream-filters.d.ts +5 -1
  126. package/dist/types/modules/pdf/types.d.ts +7 -0
  127. package/dist/types/modules/word/convert/conversion-ir.d.ts +2 -0
  128. package/dist/types/modules/word/core/text-utils.d.ts +7 -1
  129. package/dist/types/modules/word/errors.d.ts +3 -3
  130. package/dist/types/modules/word/index.base.d.ts +1 -1
  131. package/dist/types/modules/word/layout/layout-constants.d.ts +13 -9
  132. package/dist/types/modules/word/query/final-view.d.ts +26 -0
  133. package/dist/types/modules/word/query/heading.d.ts +30 -0
  134. package/dist/types/modules/word/query/numbering-counter.d.ts +47 -0
  135. package/dist/types/modules/word/query/page-field.d.ts +79 -0
  136. package/dist/types/modules/word/query/search.d.ts +8 -2
  137. package/dist/types/modules/word/query/split.d.ts +4 -1
  138. package/dist/types/modules/word/query/style-resolve.d.ts +58 -11
  139. package/dist/types/modules/word/query/style-resolver.d.ts +29 -0
  140. package/dist/types/modules/word/reader/chart-parser.d.ts +2 -2
  141. package/dist/types/modules/word/reader/comments-parser.d.ts +1 -1
  142. package/dist/types/modules/word/reader/doc-props-parsers.d.ts +4 -4
  143. package/dist/types/modules/word/reader/metadata-parsers.d.ts +2 -2
  144. package/dist/types/modules/word/reader/numbering-parser.d.ts +1 -1
  145. package/dist/types/modules/word/reader/part-xml.d.ts +14 -0
  146. package/dist/types/modules/word/reader/reader-context.d.ts +1 -1
  147. package/dist/types/modules/word/reader/settings-parser.d.ts +1 -1
  148. package/dist/types/modules/word/reader/styles-parser.d.ts +1 -1
  149. package/dist/types/modules/word/reader/theme-parser.d.ts +1 -1
  150. package/dist/types/modules/word/surface/query.d.ts +2 -1
  151. package/dist/types/modules/word/types.d.ts +5 -1
  152. package/dist/types/modules/word/writer/docx-packager.d.ts +7 -0
  153. package/dist/types/modules/word/writer/streaming-writer.d.ts +7 -0
  154. package/dist/types/modules/xml/errors.d.ts +9 -0
  155. package/dist/types/modules/xml/index.d.ts +1 -0
  156. package/dist/types/modules/xml/sax.d.ts +2 -1
  157. package/dist/types/utils/crypto.browser.d.ts +10 -0
  158. package/dist/types/utils/crypto.d.ts +10 -0
  159. package/dist/types/utils/limits.d.ts +12 -0
  160. package/package.json +1 -1
@@ -10,11 +10,11 @@
10
10
  * @see PDF Reference 1.7, §3.5 - Encryption
11
11
  * @see PDF 2.0 (ISO 32000-2), §7.6 - Encryption
12
12
  */
13
- import { pdfMd5, pdfSha256 } from "#platform/modules/pdf/core/pdf-kdf";
13
+ import { pdfMd5, pdfSha256, pdfSha384, pdfSha512 } from "#platform/modules/pdf/core/pdf-kdf";
14
14
  import { PdfStructureError } from "../errors.js";
15
15
  import { dictGetNumber, dictGetName, dictGetBytes, dictGetArray, dictGetBool } from "./pdf-parser.js";
16
16
  import { concatUint8Arrays } from "../../../utils/binary.js";
17
- import { rc4, aesCbcDecrypt, aesCbcDecryptRaw } from "#platform/utils/crypto";
17
+ import { rc4, aesCbcDecrypt, aesCbcDecryptRaw, aesCbcEncrypt, aesCbcEncryptRaw, randomBytes } from "#platform/utils/crypto";
18
18
  // =============================================================================
19
19
  // Constants
20
20
  // =============================================================================
@@ -65,8 +65,10 @@ export function initDecryption(doc, password = "") {
65
65
  if (v === 5) {
66
66
  return initDecryptionV5(doc, encryptDict, password, r, oValue, uValue, permissions, fileId);
67
67
  }
68
- // Determine if we should use AES
69
- const useAes = v === 4 && isAesCryptFilter(encryptDict);
68
+ // V4 names a crypt filter for strings and one for streams; either may be
69
+ // /Identity. Resolve them before the password so a malformed /CF fails loudly.
70
+ const stringMethod = cryptFilterMethod(encryptDict, v, "StrF");
71
+ const streamMethod = cryptFilterMethod(encryptDict, v, "StmF");
70
72
  // Try user password first, then owner password
71
73
  let encryptionKey = tryUserPassword(password, oValue, permissions, fileId, r, keyLength, uValue, encryptMetadata);
72
74
  if (!encryptionKey) {
@@ -83,14 +85,9 @@ export function initDecryption(doc, password = "") {
83
85
  if (!encryptionKey) {
84
86
  return false; // Password incorrect
85
87
  }
86
- // Set up decryption function
87
- const finalKey = encryptionKey;
88
- if (useAes) {
89
- doc.decryptFn = (data, objNum, gen) => decryptAes128(data, objNum, gen, finalKey);
90
- }
91
- else {
92
- doc.decryptFn = (data, objNum, gen) => decryptRc4PerObject(data, objNum, gen, finalKey);
93
- }
88
+ installSecurityHandler(doc, createCipher(stringMethod, encryptionKey), createCipher(streamMethod, encryptionKey),
89
+ // /EncryptMetadata is only defined from V4 on (ISO 32000-1 Table 20).
90
+ v >= 4 ? encryptMetadata : true);
94
91
  return true;
95
92
  }
96
93
  /**
@@ -107,10 +104,7 @@ export function isEncrypted(doc) {
107
104
  * Supports R=5 using SHA-256 based key derivation (Algorithm 2.A).
108
105
  */
109
106
  function initDecryptionV5(doc, encryptDict, password, revision, oValue, uValue, _permissions, _fileId) {
110
- if (revision === 6) {
111
- throw new PdfStructureError("R=6 (PDF 2.0 extension) requires SHA-384/SHA-512 which is not yet supported");
112
- }
113
- if (revision !== 5) {
107
+ if (revision !== 5 && revision !== 6) {
114
108
  throw new PdfStructureError(`Unsupported revision ${revision} for V=5 encryption`);
115
109
  }
116
110
  const oeValue = dictGetBytes(encryptDict, "OE");
@@ -125,27 +119,31 @@ function initDecryptionV5(doc, encryptDict, password, revision, oValue, uValue,
125
119
  }
126
120
  const passwordBytes = truncatePassword(password);
127
121
  // Try user password (Algorithm 2.A step a - user)
128
- let encryptionKey = tryUserPasswordV5(passwordBytes, uValue, ueValue);
122
+ // R=5 hashes once with SHA-256; R=6 uses Algorithm 2.B.
123
+ const kdf = revision === 6
124
+ ? hashR6
125
+ : (pwd, salt, udata) => pdfSha256(concatUint8Arrays([pwd, salt, udata]));
126
+ let encryptionKey = tryUserPasswordV5(passwordBytes, uValue, ueValue, kdf);
129
127
  if (!encryptionKey) {
130
128
  // Try owner password (Algorithm 2.A step a - owner)
131
- encryptionKey = tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue);
129
+ encryptionKey = tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue, kdf);
132
130
  }
133
131
  if (!encryptionKey) {
134
132
  // Try empty password
135
133
  if (password !== "") {
136
134
  const emptyBytes = new Uint8Array(0);
137
- encryptionKey = tryUserPasswordV5(emptyBytes, uValue, ueValue);
135
+ encryptionKey = tryUserPasswordV5(emptyBytes, uValue, ueValue, kdf);
138
136
  if (!encryptionKey) {
139
- encryptionKey = tryOwnerPasswordV5(emptyBytes, oValue, oeValue, uValue);
137
+ encryptionKey = tryOwnerPasswordV5(emptyBytes, oValue, oeValue, uValue, kdf);
140
138
  }
141
139
  }
142
140
  }
143
141
  if (!encryptionKey) {
144
142
  return false;
145
143
  }
146
- // V=5 always uses AES-256 with the file encryption key directly (no per-object key derivation)
147
- const finalKey = encryptionKey;
148
- doc.decryptFn = (data, _objNum, _gen) => decryptAes256Direct(data, finalKey);
144
+ // V=5 uses AES-256 with the file encryption key directly (no per-object
145
+ // key derivation), through whichever crypt filters /StrF and /StmF name.
146
+ installSecurityHandler(doc, createCipher(cryptFilterMethod(encryptDict, 5, "StrF"), encryptionKey), createCipher(cryptFilterMethod(encryptDict, 5, "StmF"), encryptionKey), readEncryptMetadata(encryptDict));
149
147
  return true;
150
148
  }
151
149
  /**
@@ -160,20 +158,18 @@ function truncatePassword(password) {
160
158
  * Validates using SHA-256(password + validation salt from U).
161
159
  * If valid, derives file encryption key using SHA-256(password + key salt from U).
162
160
  */
163
- function tryUserPasswordV5(passwordBytes, uValue, ueValue) {
161
+ function tryUserPasswordV5(passwordBytes, uValue, ueValue, kdf) {
164
162
  // U = hash(32) + validation salt(8) + key salt(8)
165
163
  const uHash = uValue.subarray(0, 32);
166
164
  const uValidationSalt = uValue.subarray(32, 40);
167
165
  const uKeySalt = uValue.subarray(40, 48);
168
166
  // Validate: SHA-256(password + validation salt) == first 32 bytes of U
169
- const validateInput = concatUint8Arrays([passwordBytes, uValidationSalt]);
170
- const computedHash = pdfSha256(validateInput);
167
+ const computedHash = kdf(passwordBytes, uValidationSalt, EMPTY);
171
168
  if (!arraysEqual(computedHash, uHash)) {
172
169
  return null;
173
170
  }
174
171
  // Derive key: SHA-256(password + key salt) => use as AES-256 key to decrypt UE
175
- const keyInput = concatUint8Arrays([passwordBytes, uKeySalt]);
176
- const keyHash = pdfSha256(keyInput);
172
+ const keyHash = kdf(passwordBytes, uKeySalt, EMPTY);
177
173
  // Decrypt UE with this key using AES-256-CBC with zero IV
178
174
  const zeroIv = new Uint8Array(16);
179
175
  return aesCbcDecryptRaw(ueValue.subarray(0, 32), keyHash, zeroIv);
@@ -183,25 +179,51 @@ function tryUserPasswordV5(passwordBytes, uValue, ueValue) {
183
179
  * Validates using SHA-256(password + validation salt from O + U(48)).
184
180
  * If valid, derives file encryption key using SHA-256(password + key salt from O + U(48)).
185
181
  */
186
- function tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue) {
182
+ function tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue, kdf) {
187
183
  // O = hash(32) + validation salt(8) + key salt(8)
188
184
  const oHash = oValue.subarray(0, 32);
189
185
  const oValidationSalt = oValue.subarray(32, 40);
190
186
  const oKeySalt = oValue.subarray(40, 48);
191
187
  const u48 = uValue.subarray(0, 48);
192
188
  // Validate: SHA-256(password + validation salt + U(0..47)) == first 32 bytes of O
193
- const validateInput = concatUint8Arrays([passwordBytes, oValidationSalt, u48]);
194
- const computedHash = pdfSha256(validateInput);
189
+ const computedHash = kdf(passwordBytes, oValidationSalt, u48);
195
190
  if (!arraysEqual(computedHash, oHash)) {
196
191
  return null;
197
192
  }
198
193
  // Derive key: SHA-256(password + key salt + U(0..47))
199
- const keyInput = concatUint8Arrays([passwordBytes, oKeySalt, u48]);
200
- const keyHash = pdfSha256(keyInput);
194
+ const keyHash = kdf(passwordBytes, oKeySalt, u48);
201
195
  // Decrypt OE with this key using AES-256-CBC with zero IV
202
196
  const zeroIv = new Uint8Array(16);
203
197
  return aesCbcDecryptRaw(oeValue.subarray(0, 32), keyHash, zeroIv);
204
198
  }
199
+ const EMPTY = new Uint8Array(0);
200
+ /**
201
+ * Algorithm 2.B (ISO 32000-2 §7.6.4.3.4), the R=6 password hash: SHA-256 of
202
+ * the input, then at least 64 rounds that AES-128-CBC encrypt 64 copies of
203
+ * `password ‖ K ‖ userData` under K and rehash with SHA-256/384/512 chosen by
204
+ * the first 16 bytes of the result mod 3.
205
+ */
206
+ function hashR6(password, salt, userData) {
207
+ let k = pdfSha256(concatUint8Arrays([password, salt, userData]));
208
+ for (let round = 0;; round++) {
209
+ const unit = concatUint8Arrays([password, k, userData]);
210
+ const k1 = new Uint8Array(unit.length * 64);
211
+ for (let i = 0; i < 64; i++) {
212
+ k1.set(unit, i * unit.length);
213
+ }
214
+ const e = aesCbcEncryptRaw(k1, k.subarray(0, 16), k.subarray(16, 32));
215
+ let sum = 0;
216
+ for (let i = 0; i < 16; i++) {
217
+ sum += e[i];
218
+ }
219
+ const pick = sum % 3;
220
+ k = pick === 0 ? pdfSha256(e) : pick === 1 ? pdfSha384(e) : pdfSha512(e);
221
+ if (round >= 63 && e[e.length - 1] <= round - 31) {
222
+ break;
223
+ }
224
+ }
225
+ return k.subarray(0, 32);
226
+ }
205
227
  /**
206
228
  * Decrypt data using AES-256 directly (no per-object key derivation).
207
229
  * For V=5, the file encryption key is used directly. The first 16 bytes are IV.
@@ -333,52 +355,47 @@ function deriveUserPasswordFromOwner(ownerPassword, oValue, revision, keyLength)
333
355
  // AES-128 Decryption
334
356
  // =============================================================================
335
357
  /**
336
- * Decrypt data using RC4 with per-object key derivation.
337
- * Per-object key = MD5(encryptionKey + objNum(3LE) + genNum(2LE)), truncated to min(n+5, 16).
358
+ * Per-object key (Algorithm 1, ISO 32000-1 §7.6.2): MD5 of the file key, the
359
+ * object number (3 bytes LE), the generation (2 bytes LE) and, for AES, the
360
+ * salt "sAlT"; truncated to min(n + 5, 16) bytes.
338
361
  */
339
- function decryptRc4PerObject(data, objectNumber, generation, encryptionKey) {
340
- const keyInput = new Uint8Array(encryptionKey.length + 5);
362
+ function objectKey(encryptionKey, objectNumber, generation, aes) {
363
+ const n = encryptionKey.length;
364
+ const keyInput = new Uint8Array(n + 5 + (aes ? 4 : 0));
341
365
  keyInput.set(encryptionKey);
342
- keyInput[encryptionKey.length] = objectNumber & 0xff;
343
- keyInput[encryptionKey.length + 1] = (objectNumber >> 8) & 0xff;
344
- keyInput[encryptionKey.length + 2] = (objectNumber >> 16) & 0xff;
345
- keyInput[encryptionKey.length + 3] = generation & 0xff;
346
- keyInput[encryptionKey.length + 4] = (generation >> 8) & 0xff;
347
- const objKey = pdfMd5(keyInput);
348
- const keyLen = Math.min(encryptionKey.length + 5, 16);
349
- return rc4(objKey.subarray(0, keyLen), data);
366
+ keyInput[n] = objectNumber & 0xff;
367
+ keyInput[n + 1] = (objectNumber >> 8) & 0xff;
368
+ keyInput[n + 2] = (objectNumber >> 16) & 0xff;
369
+ keyInput[n + 3] = generation & 0xff;
370
+ keyInput[n + 4] = (generation >> 8) & 0xff;
371
+ if (aes) {
372
+ keyInput.set([0x73, 0x41, 0x6c, 0x54], n + 5); // "sAlT"
373
+ }
374
+ return pdfMd5(keyInput).subarray(0, Math.min(n + 5, 16));
375
+ }
376
+ /** Decrypt data using RC4 with the per-object key. */
377
+ function decryptRc4PerObject(data, objectNumber, generation, encryptionKey) {
378
+ return rc4(objectKey(encryptionKey, objectNumber, generation, false), data);
350
379
  }
351
380
  /**
352
- * Decrypt data using AES-128-CBC.
381
+ * Decrypt data using AES-128-CBC with the per-object key.
353
382
  * Per PDF spec, the first 16 bytes of the data are the IV.
354
383
  */
355
384
  function decryptAes128(data, objectNumber, generation, encryptionKey) {
356
385
  if (data.length < 16) {
357
386
  return data;
358
387
  }
359
- // Compute per-object key: MD5(encryptionKey + objNum(3LE) + genNum(2LE) + "sAlT")
360
- const keyInput = new Uint8Array(encryptionKey.length + 5 + 4);
361
- keyInput.set(encryptionKey);
362
- keyInput[encryptionKey.length] = objectNumber & 0xff;
363
- keyInput[encryptionKey.length + 1] = (objectNumber >> 8) & 0xff;
364
- keyInput[encryptionKey.length + 2] = (objectNumber >> 16) & 0xff;
365
- keyInput[encryptionKey.length + 3] = generation & 0xff;
366
- keyInput[encryptionKey.length + 4] = (generation >> 8) & 0xff;
367
- // AES salt
368
- keyInput[encryptionKey.length + 5] = 0x73; // s
369
- keyInput[encryptionKey.length + 6] = 0x41; // A
370
- keyInput[encryptionKey.length + 7] = 0x6c; // l
371
- keyInput[encryptionKey.length + 8] = 0x54; // T
372
- const objKey = pdfMd5(keyInput);
373
- const keyLen = Math.min(encryptionKey.length + 5, 16);
374
- const aesKey = objKey.subarray(0, keyLen);
375
- // Extract IV (first 16 bytes) and ciphertext
376
388
  const iv = data.subarray(0, 16);
377
389
  const ciphertext = data.subarray(16);
378
390
  if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
379
391
  return data;
380
392
  }
381
- return aesCbcDecrypt(ciphertext, aesKey, iv);
393
+ return aesCbcDecrypt(ciphertext, objectKey(encryptionKey, objectNumber, generation, true), iv);
394
+ }
395
+ /** AES-CBC with PKCS#7 padding and a random IV prepended, as PDF stores it. */
396
+ function aesCbcEncryptWithIv(data, key) {
397
+ const iv = randomBytes(16);
398
+ return concatUint8Arrays([iv, aesCbcEncrypt(data, key, iv)]);
382
399
  }
383
400
  // =============================================================================
384
401
  // Helpers
@@ -428,18 +445,63 @@ function readEncryptMetadata(encryptDict) {
428
445
  return true;
429
446
  }
430
447
  /**
431
- * Check if V4 encryption uses AES (vs RC4).
448
+ * Resolve the crypt filter `/StrF` or `/StmF` names to its method. Before V4
449
+ * there are no crypt filters and everything is RC4. From V4 on an absent
450
+ * entry means `/Identity` (Table 20), and a named filter is looked up in
451
+ * `/CF` (Table 25), where `/CFM /None` also leaves the data as it is.
432
452
  */
433
- function isAesCryptFilter(encryptDict) {
453
+ function cryptFilterMethod(encryptDict, v, entry) {
454
+ if (v < 4) {
455
+ return "rc4";
456
+ }
457
+ const name = dictGetName(encryptDict, entry) ?? "Identity";
458
+ if (name === "Identity") {
459
+ return "identity";
460
+ }
434
461
  const cf = encryptDict.get("CF");
435
- if (!cf || !(cf instanceof Map)) {
436
- return false;
462
+ const filter = cf instanceof Map ? cf.get(name) : undefined;
463
+ if (!(filter instanceof Map)) {
464
+ throw new PdfStructureError(`/${entry} names crypt filter /${name}, which /CF does not define`);
465
+ }
466
+ const cfm = filter.get("CFM") ?? "None";
467
+ switch (cfm) {
468
+ case "None":
469
+ return "identity";
470
+ case "V2":
471
+ return "rc4";
472
+ case "AESV2":
473
+ return "aesv2";
474
+ case "AESV3":
475
+ return "aesv3";
476
+ default:
477
+ throw new PdfStructureError(`Unsupported crypt filter method: ${String(cfm)}`);
437
478
  }
438
- // Check StdCF filter
439
- const stdCF = cf.get("StdCF");
440
- if (!stdCF || !(stdCF instanceof Map)) {
441
- return false;
479
+ }
480
+ function createCipher(method, key) {
481
+ switch (method) {
482
+ case "identity":
483
+ return undefined;
484
+ case "rc4": {
485
+ // RC4 is its own inverse.
486
+ const apply = (data, objNum, gen) => decryptRc4PerObject(data, objNum, gen, key);
487
+ return { decrypt: apply, encrypt: apply };
488
+ }
489
+ case "aesv2":
490
+ return {
491
+ decrypt: (data, objNum, gen) => decryptAes128(data, objNum, gen, key),
492
+ encrypt: (data, objNum, gen) => aesCbcEncryptWithIv(data, objectKey(key, objNum, gen, true))
493
+ };
494
+ case "aesv3":
495
+ return {
496
+ decrypt: data => decryptAes256Direct(data, key),
497
+ encrypt: data => aesCbcEncryptWithIv(data, key)
498
+ };
442
499
  }
443
- const cfm = stdCF.get("CFM");
444
- return cfm === "AESV2";
500
+ }
501
+ /** Install the document's security handler: Identity filters pass data through. */
502
+ function installSecurityHandler(doc, strings, streams, encryptMetadata) {
503
+ const pick = (kind) => (kind === "string" ? strings : streams);
504
+ doc.decryptFn = (data, objNum, gen, kind) => pick(kind)?.decrypt(data, objNum, gen) ?? data;
505
+ doc.encryptFn = (data, objNum, gen, kind) => pick(kind)?.encrypt(data, objNum, gen) ?? data;
506
+ doc.encryptMetadata = encryptMetadata;
445
507
  }