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