@cj-tech-master/excelts 9.1.0 → 9.2.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 (147) hide show
  1. package/README.md +16 -1
  2. package/dist/browser/modules/archive/compression/crc32.js +1 -1
  3. package/dist/browser/modules/archive/crypto/aes.d.ts +0 -8
  4. package/dist/browser/modules/archive/crypto/aes.js +1 -20
  5. package/dist/browser/modules/archive/crypto/index.d.ts +2 -1
  6. package/dist/browser/modules/archive/crypto/index.js +3 -1
  7. package/dist/browser/modules/csv/parse/row-processor.d.ts +1 -1
  8. package/dist/browser/modules/csv/worker/worker-script.generated.js +1 -1
  9. package/dist/browser/modules/excel/utils/cell-matrix.js +1 -0
  10. package/dist/browser/modules/excel/utils/encryptor.browser.d.ts +4 -5
  11. package/dist/browser/modules/excel/utils/encryptor.browser.js +7 -12
  12. package/dist/browser/modules/excel/utils/encryptor.d.ts +1 -1
  13. package/dist/browser/modules/excel/utils/encryptor.js +4 -7
  14. package/dist/browser/modules/pdf/builder/document-builder.d.ts +517 -0
  15. package/dist/browser/modules/pdf/builder/document-builder.js +1493 -0
  16. package/dist/browser/modules/pdf/builder/form-appearance.d.ts +56 -0
  17. package/dist/browser/modules/pdf/builder/form-appearance.js +140 -0
  18. package/dist/browser/modules/pdf/builder/image-utils.d.ts +39 -0
  19. package/dist/browser/modules/pdf/builder/image-utils.js +129 -0
  20. package/dist/browser/modules/pdf/builder/pdf-editor.d.ts +230 -0
  21. package/dist/browser/modules/pdf/builder/pdf-editor.js +1574 -0
  22. package/dist/browser/modules/pdf/builder/resource-merger.d.ts +41 -0
  23. package/dist/browser/modules/pdf/builder/resource-merger.js +258 -0
  24. package/dist/browser/modules/pdf/core/digital-signature.d.ts +109 -0
  25. package/dist/browser/modules/pdf/core/digital-signature.js +659 -0
  26. package/dist/browser/modules/pdf/core/encryption.js +8 -7
  27. package/dist/browser/modules/pdf/core/pdf-object.d.ts +11 -0
  28. package/dist/browser/modules/pdf/core/pdf-object.js +38 -0
  29. package/dist/browser/modules/pdf/core/pdf-stream.d.ts +32 -0
  30. package/dist/browser/modules/pdf/core/pdf-stream.js +66 -0
  31. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +55 -1
  32. package/dist/browser/modules/pdf/core/pdf-writer.js +271 -6
  33. package/dist/browser/modules/pdf/core/pdfa.d.ts +62 -0
  34. package/dist/browser/modules/pdf/core/pdfa.js +261 -0
  35. package/dist/browser/modules/pdf/index.d.ts +11 -0
  36. package/dist/browser/modules/pdf/index.js +9 -0
  37. package/dist/browser/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
  38. package/dist/browser/modules/pdf/reader/bookmark-extractor.js +324 -0
  39. package/dist/browser/modules/pdf/reader/pdf-decrypt.js +6 -5
  40. package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +17 -0
  41. package/dist/browser/modules/pdf/reader/pdf-reader.js +26 -2
  42. package/dist/browser/modules/pdf/reader/table-extractor.d.ts +69 -0
  43. package/dist/browser/modules/pdf/reader/table-extractor.js +365 -0
  44. package/dist/browser/modules/pdf/render/layout-engine.d.ts +21 -1
  45. package/dist/browser/modules/pdf/render/layout-engine.js +112 -5
  46. package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -9
  47. package/dist/browser/modules/pdf/render/page-renderer.js +62 -103
  48. package/dist/browser/modules/pdf/render/pdf-exporter.js +2 -61
  49. package/dist/browser/modules/pdf/render/style-converter.d.ts +4 -0
  50. package/dist/browser/modules/pdf/render/style-converter.js +1 -1
  51. package/dist/browser/modules/pdf/types.d.ts +14 -1
  52. package/dist/browser/modules/stream/browser/readable.js +8 -2
  53. package/dist/browser/utils/crypto.browser.d.ts +64 -0
  54. package/dist/browser/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
  55. package/dist/browser/utils/crypto.d.ts +97 -0
  56. package/dist/browser/utils/crypto.js +209 -0
  57. package/dist/cjs/modules/archive/compression/crc32.js +1 -1
  58. package/dist/cjs/modules/archive/crypto/aes.js +2 -23
  59. package/dist/cjs/modules/archive/crypto/index.js +3 -1
  60. package/dist/cjs/modules/csv/worker/worker-script.generated.js +1 -1
  61. package/dist/cjs/modules/excel/utils/cell-matrix.js +1 -0
  62. package/dist/cjs/modules/excel/utils/encryptor.browser.js +7 -12
  63. package/dist/cjs/modules/excel/utils/encryptor.js +4 -10
  64. package/dist/cjs/modules/pdf/builder/document-builder.js +1532 -0
  65. package/dist/cjs/modules/pdf/builder/form-appearance.js +145 -0
  66. package/dist/cjs/modules/pdf/builder/image-utils.js +135 -0
  67. package/dist/cjs/modules/pdf/builder/pdf-editor.js +1612 -0
  68. package/dist/cjs/modules/pdf/builder/resource-merger.js +263 -0
  69. package/dist/cjs/modules/pdf/core/digital-signature.js +667 -0
  70. package/dist/cjs/modules/pdf/core/encryption.js +8 -7
  71. package/dist/cjs/modules/pdf/core/pdf-object.js +38 -0
  72. package/dist/cjs/modules/pdf/core/pdf-stream.js +66 -0
  73. package/dist/cjs/modules/pdf/core/pdf-writer.js +272 -6
  74. package/dist/cjs/modules/pdf/core/pdfa.js +266 -0
  75. package/dist/cjs/modules/pdf/index.js +19 -1
  76. package/dist/cjs/modules/pdf/reader/bookmark-extractor.js +327 -0
  77. package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +6 -5
  78. package/dist/cjs/modules/pdf/reader/pdf-reader.js +26 -2
  79. package/dist/cjs/modules/pdf/reader/table-extractor.js +368 -0
  80. package/dist/cjs/modules/pdf/render/layout-engine.js +113 -4
  81. package/dist/cjs/modules/pdf/render/page-renderer.js +63 -105
  82. package/dist/cjs/modules/pdf/render/pdf-exporter.js +3 -62
  83. package/dist/cjs/modules/pdf/render/style-converter.js +1 -0
  84. package/dist/cjs/modules/stream/browser/readable.js +8 -2
  85. package/dist/cjs/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +95 -102
  86. package/dist/cjs/utils/crypto.js +228 -0
  87. package/dist/esm/modules/archive/compression/crc32.js +1 -1
  88. package/dist/esm/modules/archive/crypto/aes.js +1 -20
  89. package/dist/esm/modules/archive/crypto/index.js +3 -1
  90. package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
  91. package/dist/esm/modules/excel/utils/cell-matrix.js +1 -0
  92. package/dist/esm/modules/excel/utils/encryptor.browser.js +7 -12
  93. package/dist/esm/modules/excel/utils/encryptor.js +4 -7
  94. package/dist/esm/modules/pdf/builder/document-builder.js +1493 -0
  95. package/dist/esm/modules/pdf/builder/form-appearance.js +140 -0
  96. package/dist/esm/modules/pdf/builder/image-utils.js +129 -0
  97. package/dist/esm/modules/pdf/builder/pdf-editor.js +1574 -0
  98. package/dist/esm/modules/pdf/builder/resource-merger.js +258 -0
  99. package/dist/esm/modules/pdf/core/digital-signature.js +659 -0
  100. package/dist/esm/modules/pdf/core/encryption.js +8 -7
  101. package/dist/esm/modules/pdf/core/pdf-object.js +38 -0
  102. package/dist/esm/modules/pdf/core/pdf-stream.js +66 -0
  103. package/dist/esm/modules/pdf/core/pdf-writer.js +271 -6
  104. package/dist/esm/modules/pdf/core/pdfa.js +261 -0
  105. package/dist/esm/modules/pdf/index.js +9 -0
  106. package/dist/esm/modules/pdf/reader/bookmark-extractor.js +324 -0
  107. package/dist/esm/modules/pdf/reader/pdf-decrypt.js +6 -5
  108. package/dist/esm/modules/pdf/reader/pdf-reader.js +26 -2
  109. package/dist/esm/modules/pdf/reader/table-extractor.js +365 -0
  110. package/dist/esm/modules/pdf/render/layout-engine.js +112 -5
  111. package/dist/esm/modules/pdf/render/page-renderer.js +62 -103
  112. package/dist/esm/modules/pdf/render/pdf-exporter.js +2 -61
  113. package/dist/esm/modules/pdf/render/style-converter.js +1 -1
  114. package/dist/esm/modules/stream/browser/readable.js +8 -2
  115. package/dist/esm/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
  116. package/dist/esm/utils/crypto.js +209 -0
  117. package/dist/iife/excelts.iife.js +1248 -1074
  118. package/dist/iife/excelts.iife.js.map +1 -1
  119. package/dist/iife/excelts.iife.min.js +53 -54
  120. package/dist/types/modules/archive/crypto/aes.d.ts +0 -8
  121. package/dist/types/modules/archive/crypto/index.d.ts +2 -1
  122. package/dist/types/modules/csv/parse/row-processor.d.ts +1 -1
  123. package/dist/types/modules/excel/utils/encryptor.browser.d.ts +4 -5
  124. package/dist/types/modules/excel/utils/encryptor.d.ts +1 -1
  125. package/dist/types/modules/pdf/builder/document-builder.d.ts +517 -0
  126. package/dist/types/modules/pdf/builder/form-appearance.d.ts +56 -0
  127. package/dist/types/modules/pdf/builder/image-utils.d.ts +39 -0
  128. package/dist/types/modules/pdf/builder/pdf-editor.d.ts +230 -0
  129. package/dist/types/modules/pdf/builder/resource-merger.d.ts +41 -0
  130. package/dist/types/modules/pdf/core/digital-signature.d.ts +109 -0
  131. package/dist/types/modules/pdf/core/pdf-object.d.ts +11 -0
  132. package/dist/types/modules/pdf/core/pdf-stream.d.ts +32 -0
  133. package/dist/types/modules/pdf/core/pdf-writer.d.ts +55 -1
  134. package/dist/types/modules/pdf/core/pdfa.d.ts +62 -0
  135. package/dist/types/modules/pdf/index.d.ts +11 -0
  136. package/dist/types/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
  137. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +17 -0
  138. package/dist/types/modules/pdf/reader/table-extractor.d.ts +69 -0
  139. package/dist/types/modules/pdf/render/layout-engine.d.ts +21 -1
  140. package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -9
  141. package/dist/types/modules/pdf/render/style-converter.d.ts +4 -0
  142. package/dist/types/modules/pdf/types.d.ts +14 -1
  143. package/dist/types/utils/crypto.browser.d.ts +64 -0
  144. package/dist/types/utils/crypto.d.ts +97 -0
  145. package/package.json +110 -111
  146. package/dist/browser/modules/pdf/core/crypto.d.ts +0 -65
  147. package/dist/types/modules/pdf/core/crypto.d.ts +0 -65
@@ -0,0 +1,667 @@
1
+ "use strict";
2
+ /**
3
+ * PDF digital signature — verification and creation.
4
+ *
5
+ * Implements:
6
+ * - ASN.1 DER decode/encode (shared codec)
7
+ * - PKCS#7 / CMS SignedData parse and build
8
+ * - X.509 certificate public key extraction
9
+ * - PDF /ByteRange extraction and hash computation
10
+ * - Signature verification (RSA PKCS#1 v1.5 + SHA-256)
11
+ * - Signature creation (with ByteRange placeholder/backfill)
12
+ *
13
+ * Uses platform-native RSA via `@utils/crypto` (node:crypto on Node,
14
+ * Web Crypto API in browsers).
15
+ *
16
+ * @see RFC 5652 — CMS (Cryptographic Message Syntax)
17
+ * @see ITU-T X.690 — ASN.1 DER encoding rules
18
+ * @see ISO 32000-2:2020 §12.8 — Digital Signatures in PDF
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.asn1Parse = asn1Parse;
22
+ exports.parseCmsSignedData = parseCmsSignedData;
23
+ exports.buildCmsSignedData = buildCmsSignedData;
24
+ exports.verifyPdfSignature = verifyPdfSignature;
25
+ exports.buildSignatureDictPlaceholder = buildSignatureDictPlaceholder;
26
+ exports.signPdf = signPdf;
27
+ const crypto_1 = require("../../../utils/crypto.js");
28
+ // =============================================================================
29
+ // ASN.1 DER — Types
30
+ // =============================================================================
31
+ /** ASN.1 tag classes. */
32
+ const ASN1_CONSTRUCTED = 0x20;
33
+ /** Common ASN.1 tags. */
34
+ const TAG_INTEGER = 0x02;
35
+ const TAG_OCTET_STRING = 0x04;
36
+ const TAG_NULL = 0x05;
37
+ const TAG_OID = 0x06;
38
+ const TAG_SEQUENCE = 0x30;
39
+ const TAG_SET = 0x31;
40
+ // =============================================================================
41
+ // ASN.1 DER — Decode
42
+ // =============================================================================
43
+ /**
44
+ * Decode a single ASN.1 DER element from `data` starting at `offset`.
45
+ * Returns the parsed node and the offset after the element.
46
+ */
47
+ function asn1Decode(data, offset) {
48
+ if (offset >= data.length) {
49
+ throw new Error("ASN.1: unexpected end of data");
50
+ }
51
+ const tag = data[offset++];
52
+ let length = data[offset++];
53
+ // Long-form length
54
+ if (length & 0x80) {
55
+ const numBytes = length & 0x7f;
56
+ length = 0;
57
+ for (let i = 0; i < numBytes; i++) {
58
+ length = (length << 8) | data[offset++];
59
+ }
60
+ }
61
+ const valueStart = offset;
62
+ const valueEnd = offset + length;
63
+ const bytes = data.subarray(valueStart, valueEnd);
64
+ const children = [];
65
+ const isConstructed = (tag & ASN1_CONSTRUCTED) !== 0;
66
+ if (isConstructed) {
67
+ let childOffset = valueStart;
68
+ while (childOffset < valueEnd) {
69
+ const result = asn1Decode(data, childOffset);
70
+ children.push(result.node);
71
+ childOffset = result.end;
72
+ }
73
+ }
74
+ return { node: { tag, bytes, children }, end: valueEnd };
75
+ }
76
+ /**
77
+ * Parse ASN.1 DER data from the root.
78
+ */
79
+ function asn1Parse(data) {
80
+ return asn1Decode(data, 0).node;
81
+ }
82
+ /**
83
+ * Parse all ASN.1 DER elements from a buffer (for SEQUENCE content with multiple children).
84
+ */
85
+ function asn1ParseAll(data) {
86
+ const nodes = [];
87
+ let offset = 0;
88
+ while (offset < data.length) {
89
+ const result = asn1Decode(data, offset);
90
+ nodes.push(result.node);
91
+ offset = result.end;
92
+ }
93
+ return nodes;
94
+ }
95
+ // =============================================================================
96
+ // ASN.1 DER — Encode
97
+ // =============================================================================
98
+ /**
99
+ * Encode an ASN.1 length in DER format.
100
+ */
101
+ function asn1EncodeLength(length) {
102
+ if (length < 0x80) {
103
+ return new Uint8Array([length]);
104
+ }
105
+ const bytes = [];
106
+ let l = length;
107
+ while (l > 0) {
108
+ bytes.unshift(l & 0xff);
109
+ l >>= 8;
110
+ }
111
+ return new Uint8Array([0x80 | bytes.length, ...bytes]);
112
+ }
113
+ /**
114
+ * Encode an ASN.1 TLV (tag-length-value).
115
+ */
116
+ function asn1Encode(tag, value) {
117
+ const length = asn1EncodeLength(value.length);
118
+ const result = new Uint8Array(1 + length.length + value.length);
119
+ result[0] = tag;
120
+ result.set(length, 1);
121
+ result.set(value, 1 + length.length);
122
+ return result;
123
+ }
124
+ /**
125
+ * Encode a SEQUENCE.
126
+ */
127
+ function asn1Sequence(...children) {
128
+ let totalLen = 0;
129
+ for (const c of children) {
130
+ totalLen += c.length;
131
+ }
132
+ const body = new Uint8Array(totalLen);
133
+ let offset = 0;
134
+ for (const c of children) {
135
+ body.set(c, offset);
136
+ offset += c.length;
137
+ }
138
+ return asn1Encode(TAG_SEQUENCE, body);
139
+ }
140
+ /**
141
+ * Encode a SET.
142
+ */
143
+ function asn1Set(...children) {
144
+ let totalLen = 0;
145
+ for (const c of children) {
146
+ totalLen += c.length;
147
+ }
148
+ const body = new Uint8Array(totalLen);
149
+ let offset = 0;
150
+ for (const c of children) {
151
+ body.set(c, offset);
152
+ offset += c.length;
153
+ }
154
+ return asn1Encode(TAG_SET, body);
155
+ }
156
+ /**
157
+ * Encode an OID.
158
+ */
159
+ function asn1Oid(oid) {
160
+ const parts = oid.split(".").map(Number);
161
+ const bytes = [40 * parts[0] + parts[1]];
162
+ for (let i = 2; i < parts.length; i++) {
163
+ let v = parts[i];
164
+ if (v < 128) {
165
+ bytes.push(v);
166
+ }
167
+ else {
168
+ const enc = [];
169
+ enc.push(v & 0x7f);
170
+ v >>= 7;
171
+ while (v > 0) {
172
+ enc.push(0x80 | (v & 0x7f));
173
+ v >>= 7;
174
+ }
175
+ enc.reverse();
176
+ bytes.push(...enc);
177
+ }
178
+ }
179
+ return asn1Encode(TAG_OID, new Uint8Array(bytes));
180
+ }
181
+ /**
182
+ * Encode an INTEGER (unsigned, from bytes).
183
+ */
184
+ function asn1Integer(value) {
185
+ // Prepend 0x00 if high bit is set (positive integer)
186
+ if (value.length > 0 && value[0] & 0x80) {
187
+ const padded = new Uint8Array(value.length + 1);
188
+ padded.set(value, 1);
189
+ return asn1Encode(TAG_INTEGER, padded);
190
+ }
191
+ return asn1Encode(TAG_INTEGER, value);
192
+ }
193
+ /**
194
+ * Encode an OCTET STRING.
195
+ */
196
+ function asn1OctetString(value) {
197
+ return asn1Encode(TAG_OCTET_STRING, value);
198
+ }
199
+ /**
200
+ * Encode a context-tagged explicit wrapper [N] EXPLICIT.
201
+ */
202
+ function asn1ContextExplicit(tagNum, value) {
203
+ return asn1Encode(0xa0 | tagNum, value);
204
+ }
205
+ // =============================================================================
206
+ // OID Constants
207
+ // =============================================================================
208
+ const OID_PKCS7_SIGNED_DATA = "1.2.840.113549.1.7.2";
209
+ const OID_PKCS7_DATA = "1.2.840.113549.1.7.1";
210
+ const OID_SHA256 = "2.16.840.1.101.3.4.2.1";
211
+ const OID_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
212
+ const OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
213
+ const OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
214
+ const OID_SIGNING_TIME = "1.2.840.113549.1.9.5";
215
+ // =============================================================================
216
+ // OID Helpers
217
+ // =============================================================================
218
+ /**
219
+ * Decode an OID from DER bytes to dotted string.
220
+ */
221
+ function decodeOid(bytes) {
222
+ if (bytes.length === 0) {
223
+ return "";
224
+ }
225
+ const parts = [Math.floor(bytes[0] / 40), bytes[0] % 40];
226
+ let value = 0;
227
+ for (let i = 1; i < bytes.length; i++) {
228
+ value = (value << 7) | (bytes[i] & 0x7f);
229
+ if ((bytes[i] & 0x80) === 0) {
230
+ parts.push(value);
231
+ value = 0;
232
+ }
233
+ }
234
+ return parts.join(".");
235
+ }
236
+ // =============================================================================
237
+ // X.509 Certificate — Public Key Extraction
238
+ // =============================================================================
239
+ /**
240
+ * Extract the SubjectPublicKeyInfo (SPKI) DER bytes from an X.509 certificate.
241
+ * This is what platform RSA verify APIs expect.
242
+ */
243
+ function extractSpkiFromCert(certDer) {
244
+ const cert = asn1Parse(certDer);
245
+ // Certificate → TBSCertificate → SubjectPublicKeyInfo
246
+ // TBSCertificate is the first child of Certificate (SEQUENCE)
247
+ const tbs = cert.children[0];
248
+ if (!tbs) {
249
+ throw new Error("Invalid X.509 certificate: missing TBSCertificate");
250
+ }
251
+ // SubjectPublicKeyInfo is at index 6 of TBSCertificate (after version, serial,
252
+ // signature alg, issuer, validity, subject). If there's an explicit [0] version
253
+ // tag, it shifts indices by 1.
254
+ let spkiIndex = 5; // without explicit version
255
+ if (tbs.children.length > 0 && (tbs.children[0].tag & 0xe0) === 0xa0) {
256
+ spkiIndex = 6; // with explicit version [0]
257
+ }
258
+ const spki = tbs.children[spkiIndex];
259
+ if (!spki || spki.tag !== TAG_SEQUENCE) {
260
+ throw new Error("Invalid X.509 certificate: missing SubjectPublicKeyInfo");
261
+ }
262
+ // Re-encode the SPKI node as DER
263
+ return asn1Encode(spki.tag, spki.bytes);
264
+ }
265
+ /**
266
+ * Parse a PKCS#7 / CMS SignedData structure from DER bytes.
267
+ * Extracts the first signer's info for verification.
268
+ */
269
+ function parseCmsSignedData(derBytes) {
270
+ const root = asn1Parse(derBytes);
271
+ // ContentInfo: SEQUENCE { contentType OID, content [0] EXPLICIT }
272
+ if (root.tag !== TAG_SEQUENCE || root.children.length < 2) {
273
+ throw new Error("Invalid PKCS#7: not a ContentInfo SEQUENCE");
274
+ }
275
+ const contentTypeNode = root.children[0];
276
+ const oid = decodeOid(contentTypeNode.bytes);
277
+ if (oid !== OID_PKCS7_SIGNED_DATA) {
278
+ throw new Error(`Invalid PKCS#7: expected SignedData OID, got ${oid}`);
279
+ }
280
+ // content [0] EXPLICIT → SignedData SEQUENCE
281
+ const contentWrapper = root.children[1];
282
+ const signedData = contentWrapper.children[0];
283
+ if (!signedData || signedData.tag !== TAG_SEQUENCE) {
284
+ throw new Error("Invalid PKCS#7: missing SignedData SEQUENCE");
285
+ }
286
+ // SignedData: version, digestAlgorithms, encapContentInfo, [0] certificates, [1] crls, signerInfos
287
+ const children = signedData.children;
288
+ // Find certificates [0] IMPLICIT
289
+ let certificate = null;
290
+ for (const child of children) {
291
+ if ((child.tag & 0xf0) === 0xa0 && (child.tag & 0x0f) === 0) {
292
+ // [0] certificates — first certificate
293
+ if (child.children.length > 0) {
294
+ const certNode = child.children[0];
295
+ certificate = asn1Encode(certNode.tag, certNode.bytes);
296
+ }
297
+ break;
298
+ }
299
+ }
300
+ if (!certificate) {
301
+ throw new Error("PKCS#7: no certificate found");
302
+ }
303
+ // Find signerInfos SET (last SET in SignedData)
304
+ let signerInfosSet = null;
305
+ for (let i = children.length - 1; i >= 0; i--) {
306
+ if (children[i].tag === TAG_SET) {
307
+ signerInfosSet = children[i];
308
+ break;
309
+ }
310
+ }
311
+ if (!signerInfosSet || signerInfosSet.children.length === 0) {
312
+ throw new Error("PKCS#7: no signerInfos found");
313
+ }
314
+ const signerInfo = signerInfosSet.children[0];
315
+ // SignerInfo: version, sid, digestAlgorithm, [0] signedAttrs, signatureAlgorithm, signature
316
+ const siChildren = signerInfo.children;
317
+ // digestAlgorithm
318
+ const digestAlgSeq = siChildren[2];
319
+ const digestAlgOid = digestAlgSeq
320
+ ? decodeOid(digestAlgSeq.children[0]?.bytes ?? new Uint8Array())
321
+ : "";
322
+ // signedAttrs [0] IMPLICIT
323
+ let signedAttrsRaw = new Uint8Array();
324
+ let messageDigest = new Uint8Array();
325
+ for (const child of siChildren) {
326
+ if ((child.tag & 0xf0) === 0xa0 && (child.tag & 0x0f) === 0) {
327
+ // Re-encode as SET OF for hash computation (per CMS spec §5.4)
328
+ signedAttrsRaw = asn1Encode(TAG_SET, child.bytes);
329
+ // Extract messageDigest attribute
330
+ const attrs = asn1ParseAll(child.bytes);
331
+ for (const attr of attrs) {
332
+ if (attr.tag !== TAG_SEQUENCE || attr.children.length < 2) {
333
+ continue;
334
+ }
335
+ const attrOid = decodeOid(attr.children[0].bytes);
336
+ if (attrOid === OID_MESSAGE_DIGEST) {
337
+ const attrValueSet = attr.children[1];
338
+ if (attrValueSet.children.length > 0) {
339
+ messageDigest = new Uint8Array(attrValueSet.children[0].bytes);
340
+ }
341
+ }
342
+ }
343
+ break;
344
+ }
345
+ }
346
+ // signature — last OCTET_STRING in signerInfo
347
+ let signature = new Uint8Array();
348
+ for (let i = siChildren.length - 1; i >= 0; i--) {
349
+ if (siChildren[i].tag === TAG_OCTET_STRING) {
350
+ signature = new Uint8Array(siChildren[i].bytes);
351
+ break;
352
+ }
353
+ }
354
+ return {
355
+ certificate,
356
+ signature,
357
+ digestAlgorithmOid: digestAlgOid,
358
+ signedAttrsRaw,
359
+ messageDigest
360
+ };
361
+ }
362
+ /**
363
+ * Build a CMS SignedData (PKCS#7) structure for a PDF signature.
364
+ *
365
+ * Uses SHA-256 for digest and RSA PKCS#1 v1.5 for signing.
366
+ * The signature is created over signed attributes that include
367
+ * the content-type, message-digest, and signing-time.
368
+ */
369
+ async function buildCmsSignedData(options) {
370
+ const { certificate, privateKey, data } = options;
371
+ // Compute message digest
372
+ const digest = (0, crypto_1.sha256)(data);
373
+ // Build signed attributes
374
+ const now = new Date();
375
+ const signingTimeStr = formatUtcTime(now);
376
+ const contentTypeAttr = asn1Sequence(asn1Oid(OID_CONTENT_TYPE), asn1Set(asn1Oid(OID_PKCS7_DATA)));
377
+ const messageDigestAttr = asn1Sequence(asn1Oid(OID_MESSAGE_DIGEST), asn1Set(asn1OctetString(digest)));
378
+ const signingTimeAttr = asn1Sequence(asn1Oid(OID_SIGNING_TIME), asn1Set(asn1Encode(0x17, new TextEncoder().encode(signingTimeStr))) // UTCTime
379
+ );
380
+ // Signed attrs as SET for DER encoding
381
+ const signedAttrsContent = concatDer(contentTypeAttr, signingTimeAttr, messageDigestAttr);
382
+ const signedAttrsForHash = asn1Encode(TAG_SET, signedAttrsContent);
383
+ // Sign the signed attributes
384
+ const signatureBytes = await (0, crypto_1.rsaSign)(privateKey, signedAttrsForHash);
385
+ // Build signed attrs as [0] IMPLICIT for embedding in SignerInfo
386
+ const signedAttrsImplicit = asn1Encode(0xa0, signedAttrsContent);
387
+ // Extract issuer and serial from certificate for SignerIdentifier
388
+ const cert = asn1Parse(certificate);
389
+ const tbs = cert.children[0];
390
+ let issuer;
391
+ let serial;
392
+ if (tbs.children[0] && (tbs.children[0].tag & 0xe0) === 0xa0) {
393
+ // Has explicit version
394
+ serial = asn1Encode(tbs.children[1].tag, tbs.children[1].bytes);
395
+ issuer = asn1Encode(tbs.children[3].tag, tbs.children[3].bytes);
396
+ }
397
+ else {
398
+ serial = asn1Encode(tbs.children[0].tag, tbs.children[0].bytes);
399
+ issuer = asn1Encode(tbs.children[2].tag, tbs.children[2].bytes);
400
+ }
401
+ // SignerInfo
402
+ const signerInfo = asn1Sequence(asn1Integer(new Uint8Array([1])), // version 1
403
+ asn1Sequence(issuer, serial), // issuerAndSerialNumber
404
+ asn1Sequence(asn1Oid(OID_SHA256), asn1Encode(TAG_NULL, new Uint8Array())), // digestAlgorithm
405
+ signedAttrsImplicit, // signedAttrs [0] IMPLICIT
406
+ asn1Sequence(asn1Oid(OID_SHA256_WITH_RSA), asn1Encode(TAG_NULL, new Uint8Array())), // signatureAlgorithm
407
+ asn1OctetString(signatureBytes) // signature
408
+ );
409
+ // SignedData
410
+ const signedData = asn1Sequence(asn1Integer(new Uint8Array([1])), // version 1
411
+ asn1Set(asn1Sequence(asn1Oid(OID_SHA256), asn1Encode(TAG_NULL, new Uint8Array()))), // digestAlgorithms
412
+ asn1Sequence(asn1Oid(OID_PKCS7_DATA)), // encapContentInfo (detached — no eContent)
413
+ asn1ContextExplicit(0, asn1Encode(cert.tag, cert.bytes)), // certificates [0]
414
+ asn1Set(signerInfo) // signerInfos
415
+ );
416
+ // ContentInfo wrapper
417
+ return asn1Sequence(asn1Oid(OID_PKCS7_SIGNED_DATA), asn1ContextExplicit(0, signedData));
418
+ }
419
+ /**
420
+ * Verify a digital signature in a PDF document.
421
+ *
422
+ * @param pdfData - The complete PDF file bytes
423
+ * @param signatureHex - The hex-encoded PKCS#7 signature from the /Contents field
424
+ * @param byteRange - The /ByteRange array [offset1, length1, offset2, length2]
425
+ */
426
+ async function verifyPdfSignature(pdfData, signatureHex, byteRange) {
427
+ try {
428
+ // Decode PKCS#7 from hex
429
+ const sigBytes = hexToBytes(signatureHex);
430
+ const cms = parseCmsSignedData(sigBytes);
431
+ // Extract the signed byte ranges from the PDF
432
+ const [off1, len1, off2, len2] = byteRange;
433
+ const range1 = pdfData.subarray(off1, off1 + len1);
434
+ const range2 = pdfData.subarray(off2, off2 + len2);
435
+ const signedData = new Uint8Array(len1 + len2);
436
+ signedData.set(range1);
437
+ signedData.set(range2, len1);
438
+ // Verify message digest using the algorithm from the signature
439
+ const computedDigest = hashByOid(cms.digestAlgorithmOid, signedData);
440
+ if (!bytesEqual(computedDigest, cms.messageDigest)) {
441
+ return {
442
+ valid: false,
443
+ coversWholeFile: checkCoversWholeFile(byteRange, pdfData.length),
444
+ digestAlgorithm: cms.digestAlgorithmOid,
445
+ reason: "Message digest mismatch — PDF content was modified after signing"
446
+ };
447
+ }
448
+ // Verify RSA signature over signed attributes
449
+ const spki = extractSpkiFromCert(cms.certificate);
450
+ const valid = await (0, crypto_1.rsaVerify)(spki, cms.signature, cms.signedAttrsRaw);
451
+ return {
452
+ valid,
453
+ coversWholeFile: checkCoversWholeFile(byteRange, pdfData.length),
454
+ digestAlgorithm: cms.digestAlgorithmOid,
455
+ reason: valid ? undefined : "RSA signature verification failed"
456
+ };
457
+ }
458
+ catch (err) {
459
+ return {
460
+ valid: false,
461
+ coversWholeFile: false,
462
+ digestAlgorithm: "",
463
+ reason: `Signature verification error: ${err instanceof Error ? err.message : String(err)}`
464
+ };
465
+ }
466
+ }
467
+ // =============================================================================
468
+ // PDF Signature Creation — ByteRange Placeholder
469
+ // =============================================================================
470
+ /**
471
+ * Estimated maximum size (in bytes) for the PKCS#7 signature hex string.
472
+ * A 2048-bit RSA signature with certificate is typically ~3000 bytes DER,
473
+ * which is ~6000 hex chars. We use 8192 to be safe.
474
+ */
475
+ const SIGNATURE_PLACEHOLDER_SIZE = 8192;
476
+ /**
477
+ * Create a PDF signature dictionary string with a placeholder /Contents.
478
+ * Returns the dict string and the placeholder that will be replaced.
479
+ *
480
+ * @param signerName - Optional signer name for /Name field
481
+ * @param reason - Optional reason for /Reason field
482
+ */
483
+ function buildSignatureDictPlaceholder(options) {
484
+ const placeholder = "0".repeat(SIGNATURE_PLACEHOLDER_SIZE * 2); // hex chars
485
+ let dict = "<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached";
486
+ dict += ` /Contents <${placeholder}>`;
487
+ dict += " /ByteRange [0 0000000000 0000000000 0000000000]"; // placeholder, will be patched
488
+ if (options?.name) {
489
+ dict += ` /Name (${escPdfString(options.name)})`;
490
+ }
491
+ if (options?.reason) {
492
+ dict += ` /Reason (${escPdfString(options.reason)})`;
493
+ }
494
+ if (options?.location) {
495
+ dict += ` /Location (${escPdfString(options.location)})`;
496
+ }
497
+ if (options?.contactInfo) {
498
+ dict += ` /ContactInfo (${escPdfString(options.contactInfo)})`;
499
+ }
500
+ // Add /M (signing time)
501
+ const now = new Date();
502
+ const m = formatPdfDate(now);
503
+ dict += ` /M (${m})`;
504
+ dict += " >>";
505
+ return { dictString: dict, placeholder };
506
+ }
507
+ /**
508
+ * Patch a PDF with a real signature after the /ByteRange placeholder has been written.
509
+ *
510
+ * @param pdfBytes - The PDF bytes with placeholder /Contents and /ByteRange
511
+ * @param certificate - DER-encoded X.509 certificate
512
+ * @param privateKey - DER-encoded PKCS#8 private key
513
+ * @returns The signed PDF bytes
514
+ */
515
+ async function signPdf(pdfBytes, certificate, privateKey) {
516
+ const result = new Uint8Array(pdfBytes);
517
+ // Find /ByteRange first — this uniquely identifies the signature dictionary
518
+ const byteRangePattern = findPattern(result, "/ByteRange [");
519
+ if (byteRangePattern === -1) {
520
+ throw new Error("signPdf: /ByteRange placeholder not found");
521
+ }
522
+ // Search for /Contents <hex> near /ByteRange (within the same object, search backwards)
523
+ // The signature dict typically has /Contents before /ByteRange, but search both directions
524
+ const searchStart = Math.max(0, byteRangePattern - 20000); // signature hex can be ~16K
525
+ const searchEnd = Math.min(result.length, byteRangePattern + 200);
526
+ let contentsPattern = -1;
527
+ const contentsBytes = new TextEncoder().encode("/Contents <");
528
+ outer: for (let i = searchStart; i < searchEnd; i++) {
529
+ for (let j = 0; j < contentsBytes.length; j++) {
530
+ if (result[i + j] !== contentsBytes[j]) {
531
+ continue outer;
532
+ }
533
+ }
534
+ contentsPattern = i;
535
+ break;
536
+ }
537
+ if (contentsPattern === -1) {
538
+ throw new Error("signPdf: /Contents placeholder not found near /ByteRange");
539
+ }
540
+ const hexStart = contentsPattern + "/Contents <".length;
541
+ // Find the closing >
542
+ let hexEnd = hexStart;
543
+ while (hexEnd < result.length && result[hexEnd] !== 0x3e /* > */) {
544
+ hexEnd++;
545
+ }
546
+ const brStart = byteRangePattern + "/ByteRange [".length;
547
+ let brEnd = brStart;
548
+ while (brEnd < result.length && result[brEnd] !== 0x5d /* ] */) {
549
+ brEnd++;
550
+ }
551
+ // Compute actual byte range: before <hex> and after <hex>
552
+ const sigDictContentsStart = hexStart - 1; // position of <
553
+ const sigDictContentsEnd = hexEnd + 1; // position after >
554
+ const byteRange = [
555
+ 0,
556
+ sigDictContentsStart,
557
+ sigDictContentsEnd,
558
+ result.length - sigDictContentsEnd
559
+ ];
560
+ // Patch the ByteRange value
561
+ const brValue = `${byteRange[0]} ${byteRange[1]} ${byteRange[2]} ${byteRange[3]}`;
562
+ const brPadded = brValue.padEnd(brEnd - brStart, " ");
563
+ for (let i = 0; i < brPadded.length; i++) {
564
+ result[brStart + i] = brPadded.charCodeAt(i);
565
+ }
566
+ // Compute the signed data from byte ranges
567
+ const range1 = result.subarray(byteRange[0], byteRange[0] + byteRange[1]);
568
+ const range2 = result.subarray(byteRange[2], byteRange[2] + byteRange[3]);
569
+ const signedData = new Uint8Array(byteRange[1] + byteRange[3]);
570
+ signedData.set(range1);
571
+ signedData.set(range2, byteRange[1]);
572
+ // Build CMS SignedData
573
+ const cms = await buildCmsSignedData({ certificate, privateKey, data: signedData });
574
+ // Hex-encode the signature
575
+ const hexSig = bytesToHex(cms).padEnd(hexEnd - hexStart, "0");
576
+ for (let i = 0; i < hexSig.length && i < hexEnd - hexStart; i++) {
577
+ result[hexStart + i] = hexSig.charCodeAt(i);
578
+ }
579
+ return result;
580
+ }
581
+ // =============================================================================
582
+ // Helpers
583
+ // =============================================================================
584
+ function hexToBytes(hex) {
585
+ const clean = hex.replace(/\s/g, "");
586
+ const bytes = new Uint8Array(clean.length / 2);
587
+ for (let i = 0; i < bytes.length; i++) {
588
+ bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
589
+ }
590
+ return bytes;
591
+ }
592
+ function bytesToHex(bytes) {
593
+ let hex = "";
594
+ for (let i = 0; i < bytes.length; i++) {
595
+ hex += bytes[i].toString(16).padStart(2, "0");
596
+ }
597
+ return hex;
598
+ }
599
+ function bytesEqual(a, b) {
600
+ if (a.length !== b.length) {
601
+ return false;
602
+ }
603
+ let diff = 0;
604
+ for (let i = 0; i < a.length; i++) {
605
+ diff |= a[i] ^ b[i];
606
+ }
607
+ return diff === 0;
608
+ }
609
+ function checkCoversWholeFile(byteRange, fileSize) {
610
+ // The two ranges should cover everything except the /Contents hex value
611
+ return byteRange[0] === 0 && byteRange[2] + byteRange[3] === fileSize;
612
+ }
613
+ /** Map digest algorithm OID to a hash function. Falls back to sha256 for unknown OIDs. */
614
+ function hashByOid(oid, data) {
615
+ switch (oid) {
616
+ case "1.3.14.3.2.26": // SHA-1
617
+ return (0, crypto_1.hash)("SHA-1", data);
618
+ case OID_SHA256: // SHA-256
619
+ return (0, crypto_1.sha256)(data);
620
+ case "2.16.840.1.101.3.4.2.2": // SHA-384
621
+ return (0, crypto_1.hash)("SHA-384", data);
622
+ case "2.16.840.1.101.3.4.2.3": // SHA-512
623
+ return (0, crypto_1.hash)("SHA-512", data);
624
+ case "1.2.840.113549.2.5": // MD5
625
+ return (0, crypto_1.md5)(data);
626
+ default:
627
+ // Fallback to SHA-256 for unrecognized OIDs
628
+ return (0, crypto_1.sha256)(data);
629
+ }
630
+ }
631
+ function findPattern(data, pattern) {
632
+ const patBytes = new TextEncoder().encode(pattern);
633
+ outer: for (let i = 0; i <= data.length - patBytes.length; i++) {
634
+ for (let j = 0; j < patBytes.length; j++) {
635
+ if (data[i + j] !== patBytes[j]) {
636
+ continue outer;
637
+ }
638
+ }
639
+ return i;
640
+ }
641
+ return -1;
642
+ }
643
+ function concatDer(...parts) {
644
+ let totalLen = 0;
645
+ for (const p of parts) {
646
+ totalLen += p.length;
647
+ }
648
+ const result = new Uint8Array(totalLen);
649
+ let offset = 0;
650
+ for (const p of parts) {
651
+ result.set(p, offset);
652
+ offset += p.length;
653
+ }
654
+ return result;
655
+ }
656
+ function escPdfString(s) {
657
+ return s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
658
+ }
659
+ function formatUtcTime(d) {
660
+ const pad = (n) => String(n).padStart(2, "0");
661
+ const yr = String(d.getUTCFullYear()).slice(-2);
662
+ return `${yr}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
663
+ }
664
+ function formatPdfDate(d) {
665
+ const pad = (n) => String(n).padStart(2, "0");
666
+ return `D:${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
667
+ }