@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.
- package/README.md +16 -1
- package/dist/browser/modules/archive/compression/crc32.js +1 -1
- package/dist/browser/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/browser/modules/archive/crypto/aes.js +1 -20
- package/dist/browser/modules/archive/crypto/index.d.ts +2 -1
- package/dist/browser/modules/archive/crypto/index.js +3 -1
- package/dist/browser/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/browser/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/browser/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/browser/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/browser/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/browser/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/browser/modules/excel/utils/encryptor.js +4 -7
- package/dist/browser/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/browser/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/browser/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/browser/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/browser/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/browser/modules/pdf/builder/image-utils.js +129 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/browser/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/browser/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/browser/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/browser/modules/pdf/core/digital-signature.js +659 -0
- package/dist/browser/modules/pdf/core/encryption.js +8 -7
- package/dist/browser/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/browser/modules/pdf/core/pdf-object.js +38 -0
- package/dist/browser/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/browser/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/browser/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/browser/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/browser/modules/pdf/core/pdfa.js +261 -0
- package/dist/browser/modules/pdf/index.d.ts +11 -0
- package/dist/browser/modules/pdf/index.js +9 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/browser/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/browser/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/browser/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/browser/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/browser/modules/pdf/render/layout-engine.js +112 -5
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/browser/modules/pdf/render/page-renderer.js +62 -103
- package/dist/browser/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/browser/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/browser/modules/pdf/render/style-converter.js +1 -1
- package/dist/browser/modules/pdf/types.d.ts +14 -1
- package/dist/browser/modules/stream/browser/readable.js +8 -2
- package/dist/browser/utils/crypto.browser.d.ts +64 -0
- package/dist/browser/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/browser/utils/crypto.d.ts +97 -0
- package/dist/browser/utils/crypto.js +209 -0
- package/dist/cjs/modules/archive/compression/crc32.js +1 -1
- package/dist/cjs/modules/archive/crypto/aes.js +2 -23
- package/dist/cjs/modules/archive/crypto/index.js +3 -1
- package/dist/cjs/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/cjs/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/cjs/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/cjs/modules/excel/utils/encryptor.js +4 -10
- package/dist/cjs/modules/pdf/builder/document-builder.js +1532 -0
- package/dist/cjs/modules/pdf/builder/form-appearance.js +145 -0
- package/dist/cjs/modules/pdf/builder/image-utils.js +135 -0
- package/dist/cjs/modules/pdf/builder/pdf-editor.js +1612 -0
- package/dist/cjs/modules/pdf/builder/resource-merger.js +263 -0
- package/dist/cjs/modules/pdf/core/digital-signature.js +667 -0
- package/dist/cjs/modules/pdf/core/encryption.js +8 -7
- package/dist/cjs/modules/pdf/core/pdf-object.js +38 -0
- package/dist/cjs/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/cjs/modules/pdf/core/pdf-writer.js +272 -6
- package/dist/cjs/modules/pdf/core/pdfa.js +266 -0
- package/dist/cjs/modules/pdf/index.js +19 -1
- package/dist/cjs/modules/pdf/reader/bookmark-extractor.js +327 -0
- package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/cjs/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/cjs/modules/pdf/reader/table-extractor.js +368 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +113 -4
- package/dist/cjs/modules/pdf/render/page-renderer.js +63 -105
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +3 -62
- package/dist/cjs/modules/pdf/render/style-converter.js +1 -0
- package/dist/cjs/modules/stream/browser/readable.js +8 -2
- package/dist/cjs/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +95 -102
- package/dist/cjs/utils/crypto.js +228 -0
- package/dist/esm/modules/archive/compression/crc32.js +1 -1
- package/dist/esm/modules/archive/crypto/aes.js +1 -20
- package/dist/esm/modules/archive/crypto/index.js +3 -1
- package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/esm/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/esm/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/esm/modules/excel/utils/encryptor.js +4 -7
- package/dist/esm/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/esm/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/esm/modules/pdf/builder/image-utils.js +129 -0
- package/dist/esm/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/esm/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/esm/modules/pdf/core/digital-signature.js +659 -0
- package/dist/esm/modules/pdf/core/encryption.js +8 -7
- package/dist/esm/modules/pdf/core/pdf-object.js +38 -0
- package/dist/esm/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/esm/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/esm/modules/pdf/core/pdfa.js +261 -0
- package/dist/esm/modules/pdf/index.js +9 -0
- package/dist/esm/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/esm/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/esm/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/esm/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +112 -5
- package/dist/esm/modules/pdf/render/page-renderer.js +62 -103
- package/dist/esm/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/esm/modules/pdf/render/style-converter.js +1 -1
- package/dist/esm/modules/stream/browser/readable.js +8 -2
- package/dist/esm/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/esm/utils/crypto.js +209 -0
- package/dist/iife/excelts.iife.js +1248 -1074
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +53 -54
- package/dist/types/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/types/modules/archive/crypto/index.d.ts +2 -1
- package/dist/types/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/types/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/types/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/types/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/types/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/types/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/types/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/types/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/types/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/types/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/types/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/types/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/types/modules/pdf/index.d.ts +11 -0
- package/dist/types/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/types/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/types/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/types/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/types/modules/pdf/types.d.ts +14 -1
- package/dist/types/utils/crypto.browser.d.ts +64 -0
- package/dist/types/utils/crypto.d.ts +97 -0
- package/package.json +110 -111
- package/dist/browser/modules/pdf/core/crypto.d.ts +0 -65
- 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.browser.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
|
|
16
|
+
import { sha256, aesCbcEncrypt, aesCbcEncryptRaw, aesEcbEncrypt, randomBytes } from "../../../utils/crypto.browser.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(
|
|
37
|
-
const uValue =
|
|
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(
|
|
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(
|
|
48
|
-
const oValue =
|
|
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(
|
|
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)
|