@patrickdk77/pdf-defuse 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/crypto.js ADDED
@@ -0,0 +1,404 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SecurityHandler = exports.rc4 = exports.Rc4 = void 0;
37
+ const crypto = __importStar(require("node:crypto"));
38
+ const objects_1 = require("./objects");
39
+ const PAD = Buffer.from('28BF4E5E4E758A4164004E56FFFA01082E2E00B6D0683E802F0CA9FE6453697A', 'hex');
40
+ const md5 = (...parts) => {
41
+ const h = crypto.createHash('md5');
42
+ for (const p of parts)
43
+ h.update(p);
44
+ return h.digest();
45
+ };
46
+ /** RC4 with state, so a stream can be decrypted in chunks. Node 24's OpenSSL 3 refuses RC4, so it is written here. */
47
+ class Rc4 {
48
+ s = new Uint8Array(256);
49
+ i = 0;
50
+ j = 0;
51
+ constructor(key) {
52
+ const s = this.s;
53
+ for (let i = 0; i < 256; i++)
54
+ s[i] = i;
55
+ for (let i = 0, j = 0; i < 256; i++) {
56
+ j = (j + s[i] + key[i % key.length]) & 255;
57
+ const t = s[i];
58
+ s[i] = s[j];
59
+ s[j] = t;
60
+ }
61
+ }
62
+ update(data) {
63
+ const s = this.s;
64
+ const out = Buffer.alloc(data.length);
65
+ let i = this.i;
66
+ let j = this.j;
67
+ for (let k = 0; k < data.length; k++) {
68
+ i = (i + 1) & 255;
69
+ j = (j + s[i]) & 255;
70
+ const t = s[i];
71
+ s[i] = s[j];
72
+ s[j] = t;
73
+ out[k] = data[k] ^ s[(s[i] + s[j]) & 255];
74
+ }
75
+ this.i = i;
76
+ this.j = j;
77
+ return out;
78
+ }
79
+ }
80
+ exports.Rc4 = Rc4;
81
+ const rc4 = (key, data) => new Rc4(key).update(data);
82
+ exports.rc4 = rc4;
83
+ function aesDecryptWhole(key, data) {
84
+ if (data.length < 16)
85
+ return Buffer.alloc(0);
86
+ const iv = data.subarray(0, 16);
87
+ const body = data.subarray(16, 16 + Math.floor((data.length - 16) / 16) * 16);
88
+ if (body.length === 0)
89
+ return Buffer.alloc(0);
90
+ const algo = key.length === 16 ? 'aes-128-cbc' : 'aes-256-cbc';
91
+ try {
92
+ const d = crypto.createDecipheriv(algo, key, iv);
93
+ return Buffer.concat([d.update(body), d.final()]);
94
+ }
95
+ catch {
96
+ const d = crypto.createDecipheriv(algo, key, iv);
97
+ d.setAutoPadding(false);
98
+ return Buffer.concat([d.update(body), d.final()]);
99
+ }
100
+ }
101
+ function hash2B(pw, salt, udata, revision) {
102
+ if (revision === 5)
103
+ return crypto
104
+ .createHash('sha256')
105
+ .update(Buffer.concat([pw, salt, udata]))
106
+ .digest();
107
+ let k = crypto
108
+ .createHash('sha256')
109
+ .update(Buffer.concat([pw, salt, udata]))
110
+ .digest();
111
+ let e = Buffer.alloc(0);
112
+ for (let round = 0;;) {
113
+ const k1 = Buffer.concat(new Array(64).fill(Buffer.concat([pw, k, udata])));
114
+ const c = crypto.createCipheriv('aes-128-cbc', k.subarray(0, 16), k.subarray(16, 32));
115
+ c.setAutoPadding(false);
116
+ e = Buffer.concat([c.update(k1), c.final()]);
117
+ let sum = 0;
118
+ for (let i = 0; i < 16; i++)
119
+ sum += e[i];
120
+ k = crypto
121
+ .createHash(['sha256', 'sha384', 'sha512'][sum % 3])
122
+ .update(e)
123
+ .digest();
124
+ round++;
125
+ if (round >= 64 && e[e.length - 1] <= round - 32)
126
+ break;
127
+ }
128
+ return k.subarray(0, 32);
129
+ }
130
+ function aes256NoIv(key, data) {
131
+ const d = crypto.createDecipheriv('aes-256-cbc', key, Buffer.alloc(16));
132
+ d.setAutoPadding(false);
133
+ return Buffer.concat([d.update(data), d.final()]);
134
+ }
135
+ const bytesOf = (v) => (v instanceof objects_1.PdfString ? v.bytes : new Uint8Array(0));
136
+ class SecurityHandler {
137
+ version;
138
+ keyBits;
139
+ fileKey;
140
+ streamMethod;
141
+ stringMethod;
142
+ embeddedFileMethod;
143
+ encryptMetadata;
144
+ cryptFilters;
145
+ unknownFilters;
146
+ constructor(version, keyBits, fileKey, streamMethod, stringMethod, embeddedFileMethod, encryptMetadata, cryptFilters,
147
+ /** Crypt filters with a /CFM this package cannot decrypt, mapped to that /CFM. Harmless until something selects one. */
148
+ unknownFilters) {
149
+ this.version = version;
150
+ this.keyBits = keyBits;
151
+ this.fileKey = fileKey;
152
+ this.streamMethod = streamMethod;
153
+ this.stringMethod = stringMethod;
154
+ this.embeddedFileMethod = embeddedFileMethod;
155
+ this.encryptMetadata = encryptMetadata;
156
+ this.cryptFilters = cryptFilters;
157
+ this.unknownFilters = unknownFilters;
158
+ }
159
+ static open(enc, id0, password) {
160
+ const filter = enc.name('Filter');
161
+ if (filter === 'Adobe.PubSec')
162
+ return { status: 'certificate-handler' };
163
+ if (filter !== 'Standard')
164
+ return { status: 'unknown-handler', detail: filter };
165
+ const V = enc.number('V') ?? 0;
166
+ const R = enc.number('R') ?? 0;
167
+ // ISO 32000-2 7.6: /EncryptMetadata, like crypt filters, means something only for V4 and V5.
168
+ const encryptMetadata = V < 4 || enc.get('EncryptMetadata') !== false;
169
+ const cryptFilters = new Map([['Identity', 'Identity']]);
170
+ const unknownFilters = new Map();
171
+ const cf = enc.get('CF');
172
+ if (cf instanceof objects_1.PdfDict) {
173
+ for (const [name, v] of cf.entries()) {
174
+ if (!(v instanceof objects_1.PdfDict))
175
+ continue;
176
+ const cfm = v.name('CFM') ?? 'None';
177
+ const m = { V2: 'RC4', AESV2: 'AES128', AESV3: 'AES256', None: 'Identity' }[cfm];
178
+ if (m)
179
+ cryptFilters.set(name, m);
180
+ else
181
+ unknownFilters.set(name, cfm);
182
+ }
183
+ }
184
+ let unusable;
185
+ const methodFor = (key, fallback) => {
186
+ if (V < 4)
187
+ return 'RC4';
188
+ const n = enc.name(key);
189
+ if (n === undefined)
190
+ return fallback ?? 'Identity';
191
+ const m = cryptFilters.get(n);
192
+ if (!m)
193
+ unusable ??= unknownFilters.get(n);
194
+ return m;
195
+ };
196
+ const stm = methodFor('StmF');
197
+ const str = methodFor('StrF');
198
+ const eff = methodFor('EFF', stm);
199
+ if (!stm || !str || !eff)
200
+ return { status: 'unknown-filter', detail: unusable };
201
+ const O = bytesOf(enc.get('O'));
202
+ const U = bytesOf(enc.get('U'));
203
+ let keyBits;
204
+ let fileKey;
205
+ let which = password === '' ? 'empty' : 'user';
206
+ if (R === 5 || R === 6) {
207
+ keyBits = 256;
208
+ const pw = Buffer.from(password, 'utf8').subarray(0, 127);
209
+ const uKey = () => {
210
+ if (!hash2B(pw, U.subarray(32, 40), Buffer.alloc(0), R).equals(Buffer.from(U.subarray(0, 32))))
211
+ return undefined;
212
+ return aes256NoIv(hash2B(pw, U.subarray(40, 48), Buffer.alloc(0), R), bytesOf(enc.get('UE')));
213
+ };
214
+ const oKey = () => {
215
+ const u48 = U.subarray(0, 48);
216
+ if (!hash2B(pw, O.subarray(32, 40), u48, R).equals(Buffer.from(O.subarray(0, 32))))
217
+ return undefined;
218
+ return aes256NoIv(hash2B(pw, O.subarray(40, 48), u48, R), bytesOf(enc.get('OE')));
219
+ };
220
+ fileKey = uKey();
221
+ if (!fileKey && password !== '') {
222
+ fileKey = oKey();
223
+ which = 'owner';
224
+ }
225
+ if (!fileKey)
226
+ return { status: 'password-required' };
227
+ }
228
+ else if (R >= 2 && R <= 4) {
229
+ const n = V === 1 ? 5 : V >= 4 ? 16 : (enc.number('Length') ?? 40) / 8;
230
+ if (!Number.isInteger(n) || n < 5 || n > 16)
231
+ return { status: 'unknown-filter', detail: `key length ${n * 8}` };
232
+ keyBits = n * 8;
233
+ const P = Buffer.alloc(4);
234
+ // /P is a 32-bit signed integer that some writers store unsigned; the key takes its low 32 bits either way.
235
+ P.writeInt32LE((enc.number('P') ?? 0) | 0);
236
+ const keyFor = (paddedUser) => {
237
+ let h = md5(paddedUser, O.subarray(0, 32), P, id0, R >= 4 && !encryptMetadata ? Buffer.from('ffffffff', 'hex') : Buffer.alloc(0));
238
+ if (R >= 3)
239
+ for (let i = 0; i < 50; i++)
240
+ h = md5(h.subarray(0, n));
241
+ return h.subarray(0, n);
242
+ };
243
+ const checkUser = (key) => {
244
+ if (R === 2)
245
+ return (0, exports.rc4)(key, PAD).equals(Buffer.from(U.subarray(0, 32)));
246
+ let x = (0, exports.rc4)(key, md5(PAD, id0));
247
+ for (let i = 1; i <= 19; i++)
248
+ x = (0, exports.rc4)(Buffer.from(key.map(b => b ^ i)), x);
249
+ return x.equals(Buffer.from(U.subarray(0, 16)));
250
+ };
251
+ const padded = Buffer.concat([Buffer.from(password, 'latin1').subarray(0, 32), PAD]).subarray(0, 32);
252
+ const asUser = keyFor(padded);
253
+ if (checkUser(asUser))
254
+ fileKey = asUser;
255
+ else if (password !== '') {
256
+ // Owner password: derive the RC4 key from it, decrypt O to recover the padded user password.
257
+ let h = md5(padded);
258
+ if (R >= 3)
259
+ for (let i = 0; i < 50; i++)
260
+ h = md5(h);
261
+ const ownerKey = h.subarray(0, n);
262
+ let userPadded;
263
+ if (R === 2)
264
+ userPadded = (0, exports.rc4)(ownerKey, O.subarray(0, 32));
265
+ else {
266
+ userPadded = Buffer.from(O.subarray(0, 32));
267
+ for (let i = 19; i >= 0; i--)
268
+ userPadded = (0, exports.rc4)(Buffer.from(ownerKey.map(b => b ^ i)), userPadded);
269
+ }
270
+ const k = keyFor(userPadded);
271
+ if (checkUser(k)) {
272
+ fileKey = k;
273
+ which = 'owner';
274
+ }
275
+ }
276
+ if (!fileKey)
277
+ return { status: 'password-required' };
278
+ }
279
+ else {
280
+ return { status: 'unknown-filter', detail: `R=${R}` };
281
+ }
282
+ return { status: 'ok', password: which, handler: new SecurityHandler(V, keyBits, fileKey, stm, str, eff, encryptMetadata, cryptFilters, unknownFilters) };
283
+ }
284
+ keyCache = new Map();
285
+ objectKey(num, gen, method) {
286
+ if (method === 'AES256')
287
+ return Buffer.from(this.fileKey);
288
+ const ck = `${num} ${gen} ${method}`;
289
+ const hit = this.keyCache.get(ck);
290
+ if (hit)
291
+ return hit;
292
+ if (this.keyCache.size > 256)
293
+ this.keyCache.clear();
294
+ const ext = Buffer.from([num & 255, (num >> 8) & 255, (num >> 16) & 255, gen & 255, (gen >> 8) & 255]);
295
+ const key = md5(this.fileKey, ext, method === 'AES128' ? Buffer.from('sAlT') : Buffer.alloc(0)).subarray(0, Math.min(this.fileKey.length + 5, 16));
296
+ this.keyCache.set(ck, key);
297
+ return key;
298
+ }
299
+ decryptWith(method, data, num, gen) {
300
+ if (method === 'Identity')
301
+ return data;
302
+ const key = this.objectKey(num, gen, method);
303
+ return method === 'RC4' ? (0, exports.rc4)(key, data) : aesDecryptWhole(key, data);
304
+ }
305
+ /** The crypt method for a stream, honoring a /Crypt filter in its own Filter array and the embedded-file default. */
306
+ methodForStream(stream) {
307
+ const dict = stream.dict;
308
+ const type = dict.name('Type');
309
+ if (type === 'XRef')
310
+ return 'Identity';
311
+ if (type === 'Metadata' && !this.encryptMetadata)
312
+ return 'Identity';
313
+ const f = dict.get('Filter');
314
+ const filters = f instanceof objects_1.PdfName ? [f] : Array.isArray(f) ? f : [];
315
+ // Before V4 there are no crypt filters, so a /Crypt entry cannot exempt a stream from the document's method.
316
+ const ci = this.version >= 4 ? filters.findIndex(x => x instanceof objects_1.PdfName && x.name === 'Crypt') : -1;
317
+ if (ci >= 0) {
318
+ const p = dict.get('DecodeParms');
319
+ const parms = Array.isArray(p) ? p[ci] : p;
320
+ const name = parms instanceof objects_1.PdfDict ? (parms.name('Name') ?? 'Identity') : 'Identity';
321
+ const unknown = this.unknownFilters.get(name);
322
+ if (unknown !== undefined)
323
+ throw new Error(`Crypt filter ${name} uses the unsupported method ${unknown}`);
324
+ return this.cryptFilters.get(name) ?? 'Identity';
325
+ }
326
+ if (type === 'EmbeddedFile')
327
+ return this.embeddedFileMethod;
328
+ return this.streamMethod;
329
+ }
330
+ /**
331
+ * Plaintext length of a stream, read without decrypting the whole body. For AES the last two
332
+ * blocks are decrypted to learn the padding length.
333
+ */
334
+ async plainLength(stream, num, gen, readRaw) {
335
+ const method = this.methodForStream(stream);
336
+ const len = stream.length;
337
+ if (method === 'Identity' || method === 'RC4')
338
+ return len;
339
+ if (len < 32)
340
+ return 0;
341
+ const full = 16 + Math.floor((len - 16) / 16) * 16;
342
+ const tail = await readRaw(full - 32, 32);
343
+ const key = this.objectKey(num, gen, method);
344
+ const d = crypto.createDecipheriv(key.length === 16 ? 'aes-128-ecb' : 'aes-256-ecb', key, null);
345
+ d.setAutoPadding(false);
346
+ const last = Buffer.concat([d.update(tail.subarray(16, 32)), d.final()]);
347
+ for (let i = 0; i < 16; i++)
348
+ last[i] ^= tail[i];
349
+ const pad = last[15];
350
+ const valid = pad >= 1 && pad <= 16 && last.subarray(16 - pad).every(b => b === pad);
351
+ return full - 16 - (valid ? pad : 0);
352
+ }
353
+ /** A decryptor that emits exactly `plainLength` bytes across all chunks. */
354
+ chunkDecryptor(stream, num, gen, plainLength) {
355
+ const method = this.methodForStream(stream);
356
+ if (method === 'Identity')
357
+ return { update: c => Buffer.from(c), final: () => Buffer.alloc(0) };
358
+ const key = this.objectKey(num, gen, method);
359
+ if (method === 'RC4') {
360
+ const r = new Rc4(key);
361
+ return { update: c => r.update(c), final: () => Buffer.alloc(0) };
362
+ }
363
+ let iv = null;
364
+ let pending = Buffer.alloc(0);
365
+ let decipher = null;
366
+ let emitted = 0;
367
+ const take = (b) => {
368
+ const room = plainLength - emitted;
369
+ const out = b.length > room ? b.subarray(0, room) : b;
370
+ emitted += out.length;
371
+ return out;
372
+ };
373
+ return {
374
+ update(chunk) {
375
+ let c = Buffer.from(chunk);
376
+ if (!iv) {
377
+ pending = Buffer.concat([pending, c]);
378
+ if (pending.length < 16)
379
+ return Buffer.alloc(0);
380
+ iv = pending.subarray(0, 16);
381
+ c = pending.subarray(16);
382
+ decipher = crypto.createDecipheriv(key.length === 16 ? 'aes-128-cbc' : 'aes-256-cbc', key, iv);
383
+ decipher.setAutoPadding(false);
384
+ }
385
+ if (!decipher)
386
+ throw new Error('No AES decipher after the IV was read');
387
+ return take(decipher.update(c));
388
+ },
389
+ final() {
390
+ if (!decipher)
391
+ return Buffer.alloc(0);
392
+ let rest;
393
+ try {
394
+ rest = decipher.final();
395
+ }
396
+ catch {
397
+ rest = Buffer.alloc(0);
398
+ }
399
+ return take(rest);
400
+ },
401
+ };
402
+ }
403
+ }
404
+ exports.SecurityHandler = SecurityHandler;
@@ -0,0 +1,28 @@
1
+ import type { ByteSink, ContainedFilePlugin } from './types';
2
+ /** What the CSV and TSV plugins do with a cell a spreadsheet would read as a formula. */
3
+ export type FormulaHandling = 'escape' | 'keep' | 'remove';
4
+ export interface DelimitedOptions {
5
+ /**
6
+ * 'escape', the default, puts an apostrophe before the cell so a spreadsheet shows it as text. 'keep' leaves
7
+ * it. 'remove' drops the whole file. It is read for each file, so it can still change after the plugin is made.
8
+ */
9
+ formulas?: FormulaHandling;
10
+ }
11
+ /**
12
+ * Reads CSV or TSV bytes and counts the cells a spreadsheet would run as a formula: those starting with =, +, -,
13
+ * @, tab or CR, except a plain number or a lone sign. Quoting follows RFC 4180, and a stray quote counts as text,
14
+ * as spreadsheets take it. With a sink, writes the file with an apostrophe before each such cell and every other
15
+ * byte unchanged. Returns the count, or -1 when the bytes are not UTF-8 text or a quoted cell never closes.
16
+ */
17
+ export declare function scanDelimited(chunks: AsyncIterable<Uint8Array>, delimiter: number, sink?: ByteSink): Promise<number>;
18
+ /** Keeps attached CSV files that are UTF-8 text, and escapes cells a spreadsheet would run as formulas. */
19
+ export declare function csvPlugin(options?: DelimitedOptions): ContainedFilePlugin;
20
+ /** The same for tab-separated files. */
21
+ export declare function tsvPlugin(options?: DelimitedOptions): ContainedFilePlugin;
22
+ /**
23
+ * True when the bytes are exactly one JSON value (RFC 8259) in UTF-8, after an optional byte order mark. Reads in
24
+ * chunks, keeping one byte per open container, so memory does not grow with the file.
25
+ */
26
+ export declare function isJson(chunks: AsyncIterable<Uint8Array>): Promise<boolean>;
27
+ /** Keeps attached JSON files that parse, and removes the rest. */
28
+ export declare function jsonPlugin(): ContainedFilePlugin;