rn-pdf-decrypt 1.0.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/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/crypto-aes.js +192 -0
- package/dist/crypto-aes.mjs +190 -0
- package/dist/crypto-rc4.js +185 -0
- package/dist/crypto-rc4.mjs +183 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +5 -0
- package/dist/index.mjs +3 -0
- package/dist/pdf-decrypt.js +883 -0
- package/dist/pdf-decrypt.mjs +886 -0
- package/package.json +68 -0
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rn-pdf-decrypt — PDF decryption with AES-256 and RC4 support
|
|
3
|
+
* React Native compatible fork of @pdfsmaller/pdf-decrypt
|
|
4
|
+
*
|
|
5
|
+
* @author imdewan (https://github.com/imdewan/rn-pdf-decrypt)
|
|
6
|
+
* @license MIT
|
|
7
|
+
* @see https://github.com/imdewan/rn-pdf-decrypt
|
|
8
|
+
*
|
|
9
|
+
* Implements:
|
|
10
|
+
* - AES-256 (V=5, R=5) per Adobe Supplement to ISO 32000 — SHA-256 based
|
|
11
|
+
* - AES-256 (V=5, R=6) per ISO 32000-2:2020 — Algorithms 2.A, 2.B, 11, 12, 13
|
|
12
|
+
* - RC4 128-bit (V=2, R=3) per ISO 32000-1:2008 — Algorithms 2, 4, 5, 7
|
|
13
|
+
* - RC4 40-bit (V=1, R=2) per ISO 32000-1:2008
|
|
14
|
+
*
|
|
15
|
+
* Fork of @pdfsmaller/pdf-decrypt with @noble/hashes + @noble/ciphers for React Native (Hermes) compatibility
|
|
16
|
+
* Verified against mozilla/pdf.js and Adobe Acrobat
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { PDFDocument, PDFName, PDFHexString, PDFString, PDFDict, PDFArray, PDFRawStream, PDFNumber, PDFRef } = require('pdf-lib');
|
|
20
|
+
const { md5, RC4, hexToBytes, bytesToHex } = require('./crypto-rc4.js');
|
|
21
|
+
const { sha256, aes256CbcDecrypt, aes256CbcDecryptNoPad, aes256EcbDecryptBlock, importAES256DecryptKey, aes256CbcDecryptWithKey, computeHash2B, concat } = require('./crypto-aes.js');
|
|
22
|
+
|
|
23
|
+
// ========== Constants ==========
|
|
24
|
+
|
|
25
|
+
// Standard PDF padding string (from PDF specification)
|
|
26
|
+
const PADDING = new Uint8Array([
|
|
27
|
+
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
|
|
28
|
+
0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
|
|
29
|
+
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
|
|
30
|
+
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Batch size for parallel AES-256 decryption
|
|
34
|
+
const BATCH_SIZE = 100;
|
|
35
|
+
|
|
36
|
+
// ========== Helper Functions ==========
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compare two Uint8Arrays for equality
|
|
40
|
+
*/
|
|
41
|
+
function arraysEqual(a, b) {
|
|
42
|
+
if (a.length !== b.length) return false;
|
|
43
|
+
for (let i = 0; i < a.length; i++) {
|
|
44
|
+
if (a[i] !== b[i]) return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract bytes from a PDF string value (handles both hex and literal strings)
|
|
51
|
+
*/
|
|
52
|
+
function extractBytes(pdfObj) {
|
|
53
|
+
if (!pdfObj) return null;
|
|
54
|
+
|
|
55
|
+
if (pdfObj instanceof PDFHexString) {
|
|
56
|
+
return hexToBytes(pdfObj.asString());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (pdfObj instanceof PDFString) {
|
|
60
|
+
return pdfObj.asBytes();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle raw string representation (fallback)
|
|
64
|
+
const str = pdfObj.toString();
|
|
65
|
+
if (str.startsWith('<') && str.endsWith('>')) {
|
|
66
|
+
return hexToBytes(str.slice(1, -1));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Truncate password to 127 bytes (UTF-8) per PDF 2.0 spec
|
|
74
|
+
*/
|
|
75
|
+
function saslPrepPassword(password) {
|
|
76
|
+
const bytes = new TextEncoder().encode(password);
|
|
77
|
+
return bytes.length > 127 ? bytes.slice(0, 127) : bytes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ========== Encryption Parameter Reading ==========
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read the encryption parameters from a PDF's trailer
|
|
84
|
+
* Supports V=1-2 (RC4) and V=5 (AES-256)
|
|
85
|
+
*/
|
|
86
|
+
function readEncryptParams(context) {
|
|
87
|
+
const trailer = context.trailerInfo;
|
|
88
|
+
|
|
89
|
+
// Get /Encrypt reference
|
|
90
|
+
const encryptRef = trailer.Encrypt;
|
|
91
|
+
if (!encryptRef) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Resolve the encrypt dictionary
|
|
96
|
+
let encryptDict;
|
|
97
|
+
if (encryptRef instanceof PDFRef) {
|
|
98
|
+
encryptDict = context.lookup(encryptRef);
|
|
99
|
+
} else if (encryptRef instanceof PDFDict) {
|
|
100
|
+
encryptDict = encryptRef;
|
|
101
|
+
} else {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!encryptDict || !(encryptDict instanceof PDFDict)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Read basic encryption parameters
|
|
110
|
+
const V = encryptDict.get(PDFName.of('V'));
|
|
111
|
+
const R = encryptDict.get(PDFName.of('R'));
|
|
112
|
+
const Length = encryptDict.get(PDFName.of('Length'));
|
|
113
|
+
const P = encryptDict.get(PDFName.of('P'));
|
|
114
|
+
const O = encryptDict.get(PDFName.of('O'));
|
|
115
|
+
const U = encryptDict.get(PDFName.of('U'));
|
|
116
|
+
|
|
117
|
+
const version = V ? (typeof V.asNumber === 'function' ? V.asNumber() : Number(V.toString())) : 0;
|
|
118
|
+
const revision = R ? (typeof R.asNumber === 'function' ? R.asNumber() : Number(R.toString())) : 0;
|
|
119
|
+
|
|
120
|
+
// Extract permissions (signed 32-bit integer)
|
|
121
|
+
const permissions = P ? (typeof P.asNumber === 'function' ? P.asNumber() : Number(P.toString())) : 0;
|
|
122
|
+
|
|
123
|
+
// Extract O and U values
|
|
124
|
+
const ownerKey = extractBytes(O);
|
|
125
|
+
const userKey = extractBytes(U);
|
|
126
|
+
|
|
127
|
+
if (!ownerKey || !userKey) {
|
|
128
|
+
throw new Error('Could not read /O or /U values from encryption dictionary');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Extract file ID
|
|
132
|
+
let fileId = new Uint8Array(0);
|
|
133
|
+
const idArray = trailer.ID;
|
|
134
|
+
|
|
135
|
+
if (idArray) {
|
|
136
|
+
if (Array.isArray(idArray) && idArray.length > 0) {
|
|
137
|
+
fileId = extractBytes(idArray[0]) || new Uint8Array(0);
|
|
138
|
+
} else if (idArray instanceof PDFArray) {
|
|
139
|
+
const firstId = idArray.lookup(0);
|
|
140
|
+
fileId = extractBytes(firstId) || new Uint8Array(0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Base params (shared by RC4 and AES-256)
|
|
145
|
+
const params = {
|
|
146
|
+
version,
|
|
147
|
+
revision,
|
|
148
|
+
ownerKey,
|
|
149
|
+
userKey,
|
|
150
|
+
permissions,
|
|
151
|
+
fileId,
|
|
152
|
+
encryptRef,
|
|
153
|
+
encryptDict
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// V=5, R=5/6: AES-256 specific fields
|
|
157
|
+
if (version === 5 && (revision === 5 || revision === 6)) {
|
|
158
|
+
const OE = encryptDict.get(PDFName.of('OE'));
|
|
159
|
+
const UE = encryptDict.get(PDFName.of('UE'));
|
|
160
|
+
const Perms = encryptDict.get(PDFName.of('Perms'));
|
|
161
|
+
const EncryptMetadata = encryptDict.get(PDFName.of('EncryptMetadata'));
|
|
162
|
+
|
|
163
|
+
params.ownerEncryptKey = extractBytes(OE);
|
|
164
|
+
params.userEncryptKey = extractBytes(UE);
|
|
165
|
+
params.perms = extractBytes(Perms);
|
|
166
|
+
|
|
167
|
+
if (!params.ownerEncryptKey || !params.userEncryptKey || !params.perms) {
|
|
168
|
+
throw new Error('Missing /OE, /UE, or /Perms in AES-256 encryption dictionary');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// EncryptMetadata defaults to true per spec
|
|
172
|
+
if (EncryptMetadata) {
|
|
173
|
+
const emStr = EncryptMetadata.toString();
|
|
174
|
+
params.encryptMetadata = emStr !== 'false';
|
|
175
|
+
} else {
|
|
176
|
+
params.encryptMetadata = true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
params.algorithm = 'AES-256';
|
|
180
|
+
params.keyLength = 32;
|
|
181
|
+
} else if (version <= 3 && revision <= 4) {
|
|
182
|
+
// RC4 (V=1-2, R=2-3)
|
|
183
|
+
let keyLengthBits = Length ? (typeof Length.asNumber === 'function' ? Length.asNumber() : Number(Length.toString())) : 40;
|
|
184
|
+
if (revision >= 3 && !Length) keyLengthBits = 128;
|
|
185
|
+
params.keyLength = keyLengthBits / 8;
|
|
186
|
+
params.algorithm = 'RC4';
|
|
187
|
+
} else {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Unsupported encryption: V=${version}, R=${revision}. ` +
|
|
190
|
+
`pdf-decrypt supports RC4 (V=1-2, R=2-3) and AES-256 (V=5, R=5/6).`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return params;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ========== RC4 Password Validation & Decryption ==========
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Pad or truncate password according to PDF spec (RC4)
|
|
201
|
+
*/
|
|
202
|
+
function padPassword(password) {
|
|
203
|
+
const pwdBytes = typeof password === 'string' ? new TextEncoder().encode(password) : password;
|
|
204
|
+
const padded = new Uint8Array(32);
|
|
205
|
+
|
|
206
|
+
if (pwdBytes.length >= 32) {
|
|
207
|
+
padded.set(pwdBytes.slice(0, 32));
|
|
208
|
+
} else {
|
|
209
|
+
padded.set(pwdBytes);
|
|
210
|
+
padded.set(PADDING.slice(0, 32 - pwdBytes.length), pwdBytes.length);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return padded;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Compute encryption key (Algorithm 2 from PDF spec)
|
|
218
|
+
* Works for both Rev 2 (RC4-40) and Rev 3 (RC4-128)
|
|
219
|
+
*/
|
|
220
|
+
function computeEncryptionKey(password, ownerKey, permissions, fileId, revision, keyLength) {
|
|
221
|
+
const paddedPwd = padPassword(password);
|
|
222
|
+
|
|
223
|
+
const hashInput = new Uint8Array(
|
|
224
|
+
paddedPwd.length +
|
|
225
|
+
ownerKey.length +
|
|
226
|
+
4 + // permissions
|
|
227
|
+
fileId.length
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
let offset = 0;
|
|
231
|
+
hashInput.set(paddedPwd, offset);
|
|
232
|
+
offset += paddedPwd.length;
|
|
233
|
+
|
|
234
|
+
hashInput.set(ownerKey, offset);
|
|
235
|
+
offset += ownerKey.length;
|
|
236
|
+
|
|
237
|
+
// Add permissions (low-order byte first)
|
|
238
|
+
hashInput[offset++] = permissions & 0xFF;
|
|
239
|
+
hashInput[offset++] = (permissions >> 8) & 0xFF;
|
|
240
|
+
hashInput[offset++] = (permissions >> 16) & 0xFF;
|
|
241
|
+
hashInput[offset++] = (permissions >> 24) & 0xFF;
|
|
242
|
+
|
|
243
|
+
hashInput.set(fileId, offset);
|
|
244
|
+
|
|
245
|
+
let hash = md5(hashInput);
|
|
246
|
+
|
|
247
|
+
// For Rev 3+, do 50 additional MD5 iterations
|
|
248
|
+
if (revision >= 3) {
|
|
249
|
+
const n = keyLength;
|
|
250
|
+
for (let i = 0; i < 50; i++) {
|
|
251
|
+
hash = md5(hash.slice(0, n));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return hash.slice(0, keyLength);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Validate user password — Algorithm 4 (Rev 2) / Algorithm 5 (Rev 3)
|
|
260
|
+
* Returns the encryption key if valid, null if invalid
|
|
261
|
+
*/
|
|
262
|
+
function validateUserPasswordRC4(password, encryptParams) {
|
|
263
|
+
const { ownerKey, userKey, permissions, fileId, revision, keyLength } = encryptParams;
|
|
264
|
+
|
|
265
|
+
const encryptionKey = computeEncryptionKey(password, ownerKey, permissions, fileId, revision, keyLength);
|
|
266
|
+
|
|
267
|
+
if (revision === 2) {
|
|
268
|
+
// Algorithm 4: RC4 encrypt the padding with the key, compare to /U
|
|
269
|
+
const rc4 = new RC4(encryptionKey);
|
|
270
|
+
const computed = rc4.process(new Uint8Array(PADDING));
|
|
271
|
+
|
|
272
|
+
if (arraysEqual(computed, userKey)) {
|
|
273
|
+
return encryptionKey;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// Algorithm 5 (Rev 3):
|
|
277
|
+
// 1. MD5(PADDING + fileId)
|
|
278
|
+
const hashInput = new Uint8Array(PADDING.length + fileId.length);
|
|
279
|
+
hashInput.set(PADDING);
|
|
280
|
+
hashInput.set(fileId, PADDING.length);
|
|
281
|
+
const hash = md5(hashInput);
|
|
282
|
+
|
|
283
|
+
// 2. RC4 encrypt with key, then 19 more iterations with key XOR i
|
|
284
|
+
let result = new RC4(encryptionKey).process(hash);
|
|
285
|
+
for (let i = 1; i <= 19; i++) {
|
|
286
|
+
const iterKey = new Uint8Array(encryptionKey.length);
|
|
287
|
+
for (let j = 0; j < encryptionKey.length; j++) {
|
|
288
|
+
iterKey[j] = encryptionKey[j] ^ i;
|
|
289
|
+
}
|
|
290
|
+
result = new RC4(iterKey).process(result);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 3. Compare first 16 bytes with stored /U
|
|
294
|
+
if (arraysEqual(result.slice(0, 16), userKey.slice(0, 16))) {
|
|
295
|
+
return encryptionKey;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Validate owner password — Algorithm 7 (Rev 3) / simplified for Rev 2
|
|
304
|
+
* Recovers the user password from the owner password, then validates as user
|
|
305
|
+
* Returns the encryption key if valid, null if invalid
|
|
306
|
+
*/
|
|
307
|
+
function validateOwnerPasswordRC4(ownerPassword, encryptParams) {
|
|
308
|
+
const { ownerKey, revision, keyLength } = encryptParams;
|
|
309
|
+
|
|
310
|
+
// Step 1: Pad the owner password
|
|
311
|
+
const paddedOwner = padPassword(ownerPassword);
|
|
312
|
+
|
|
313
|
+
// Step 2: MD5 hash
|
|
314
|
+
let hash = md5(paddedOwner);
|
|
315
|
+
|
|
316
|
+
// Step 3: For Rev 3+, 50 additional MD5 iterations
|
|
317
|
+
if (revision >= 3) {
|
|
318
|
+
for (let i = 0; i < 50; i++) {
|
|
319
|
+
hash = md5(hash);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ownerDecryptKey = hash.slice(0, keyLength);
|
|
324
|
+
|
|
325
|
+
// Step 4: Decrypt /O to recover user password
|
|
326
|
+
let recoveredUserPwd;
|
|
327
|
+
|
|
328
|
+
if (revision === 2) {
|
|
329
|
+
// Simple RC4 decrypt
|
|
330
|
+
const rc4 = new RC4(ownerDecryptKey);
|
|
331
|
+
recoveredUserPwd = rc4.process(new Uint8Array(ownerKey));
|
|
332
|
+
} else {
|
|
333
|
+
// Rev 3: Reverse the 20-iteration RC4 (i = 19 → 0)
|
|
334
|
+
let result = new Uint8Array(ownerKey);
|
|
335
|
+
for (let i = 19; i >= 0; i--) {
|
|
336
|
+
const iterKey = new Uint8Array(ownerDecryptKey.length);
|
|
337
|
+
for (let j = 0; j < ownerDecryptKey.length; j++) {
|
|
338
|
+
iterKey[j] = ownerDecryptKey[j] ^ i;
|
|
339
|
+
}
|
|
340
|
+
result = new RC4(iterKey).process(result);
|
|
341
|
+
}
|
|
342
|
+
recoveredUserPwd = result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Step 5: Use recovered user password to validate
|
|
346
|
+
return validateUserPasswordRC4(recoveredUserPwd, encryptParams);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Decrypt data for a specific object using RC4
|
|
351
|
+
*/
|
|
352
|
+
function decryptObjectRC4(data, objectNum, generationNum, encryptionKey) {
|
|
353
|
+
// Create object-specific key
|
|
354
|
+
const keyInput = new Uint8Array(encryptionKey.length + 5);
|
|
355
|
+
keyInput.set(encryptionKey);
|
|
356
|
+
|
|
357
|
+
// Add object number (low byte first)
|
|
358
|
+
keyInput[encryptionKey.length] = objectNum & 0xFF;
|
|
359
|
+
keyInput[encryptionKey.length + 1] = (objectNum >> 8) & 0xFF;
|
|
360
|
+
keyInput[encryptionKey.length + 2] = (objectNum >> 16) & 0xFF;
|
|
361
|
+
|
|
362
|
+
// Add generation number (low byte first)
|
|
363
|
+
keyInput[encryptionKey.length + 3] = generationNum & 0xFF;
|
|
364
|
+
keyInput[encryptionKey.length + 4] = (generationNum >> 8) & 0xFF;
|
|
365
|
+
|
|
366
|
+
// Hash to get object key
|
|
367
|
+
const objectKey = md5(keyInput);
|
|
368
|
+
|
|
369
|
+
// Use up to 16 bytes of the hash as the key
|
|
370
|
+
const rc4 = new RC4(objectKey.slice(0, Math.min(encryptionKey.length + 5, 16)));
|
|
371
|
+
|
|
372
|
+
return rc4.process(data);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Recursively decrypt strings in a PDF object (RC4 mode)
|
|
377
|
+
*/
|
|
378
|
+
function decryptStringsRC4(obj, objectNum, generationNum, encryptionKey) {
|
|
379
|
+
if (!obj) return;
|
|
380
|
+
|
|
381
|
+
if (obj instanceof PDFString) {
|
|
382
|
+
const originalBytes = obj.asBytes();
|
|
383
|
+
const decrypted = decryptObjectRC4(originalBytes, objectNum, generationNum, encryptionKey);
|
|
384
|
+
// Convert bytes back to string via charCode (NOT bytesToHex)
|
|
385
|
+
obj.value = Array.from(decrypted).map(b => String.fromCharCode(b)).join('');
|
|
386
|
+
} else if (obj instanceof PDFHexString) {
|
|
387
|
+
const originalBytes = obj.asBytes();
|
|
388
|
+
const decrypted = decryptObjectRC4(originalBytes, objectNum, generationNum, encryptionKey);
|
|
389
|
+
obj.value = bytesToHex(decrypted);
|
|
390
|
+
} else if (obj instanceof PDFDict) {
|
|
391
|
+
const entries = obj.entries();
|
|
392
|
+
for (const [key, value] of entries) {
|
|
393
|
+
const keyName = key.asString();
|
|
394
|
+
// Skip encryption-related entries
|
|
395
|
+
if (keyName !== '/Length' && keyName !== '/Filter' && keyName !== '/DecodeParms') {
|
|
396
|
+
decryptStringsRC4(value, objectNum, generationNum, encryptionKey);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} else if (obj instanceof PDFArray) {
|
|
400
|
+
const array = obj.asArray();
|
|
401
|
+
for (const element of array) {
|
|
402
|
+
decryptStringsRC4(element, objectNum, generationNum, encryptionKey);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ========== AES-256 Password Validation ==========
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Compute the password hash for AES-256, dispatching to the correct algorithm
|
|
411
|
+
* based on revision:
|
|
412
|
+
* - R=5 (Adobe extension): SHA-256(password + salt + extra)
|
|
413
|
+
* - R=6 (ISO 32000-2): Algorithm 2.B (iterative hash)
|
|
414
|
+
*/
|
|
415
|
+
async function aes256PasswordHash(password, salt, extra, revision) {
|
|
416
|
+
if (revision === 5) {
|
|
417
|
+
return sha256(concat(password, salt, extra));
|
|
418
|
+
}
|
|
419
|
+
return computeHash2B(password, salt, extra);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Validate user password for AES-256 (R=5: Adobe extension / R=6: Algorithm 11)
|
|
424
|
+
*
|
|
425
|
+
* Steps:
|
|
426
|
+
* 1. hash(password, U[32:40], []) → compare with U[0:32]
|
|
427
|
+
* 2. If match: hash(password, U[40:48], []) → decrypt UE → file key
|
|
428
|
+
*
|
|
429
|
+
* @returns {Promise<Uint8Array|null>} - 32-byte file key if valid, null if invalid
|
|
430
|
+
*/
|
|
431
|
+
async function validateUserPasswordAES256(password, encryptParams) {
|
|
432
|
+
const { userKey, userEncryptKey, revision } = encryptParams;
|
|
433
|
+
|
|
434
|
+
// U validation salt = U[32:40]
|
|
435
|
+
const validationSalt = userKey.slice(32, 40);
|
|
436
|
+
const hash = await aes256PasswordHash(password, validationSalt, new Uint8Array(0), revision);
|
|
437
|
+
|
|
438
|
+
// Compare hash with U[0:32]
|
|
439
|
+
if (!arraysEqual(hash, userKey.slice(0, 32))) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Password valid — derive file key from UE
|
|
444
|
+
// Key salt = U[40:48]
|
|
445
|
+
const keySalt = userKey.slice(40, 48);
|
|
446
|
+
const ueKey = await aes256PasswordHash(password, keySalt, new Uint8Array(0), revision);
|
|
447
|
+
const zeroIV = new Uint8Array(16);
|
|
448
|
+
const fileKey = await aes256CbcDecryptNoPad(userEncryptKey, ueKey, zeroIV);
|
|
449
|
+
|
|
450
|
+
return fileKey;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Validate owner password for AES-256 (R=5: Adobe extension / R=6: Algorithm 12)
|
|
455
|
+
*
|
|
456
|
+
* Steps:
|
|
457
|
+
* 1. hash(password, O[32:40], U) → compare with O[0:32]
|
|
458
|
+
* 2. If match: hash(password, O[40:48], U) → decrypt OE → file key
|
|
459
|
+
*
|
|
460
|
+
* @returns {Promise<Uint8Array|null>} - 32-byte file key if valid, null if invalid
|
|
461
|
+
*/
|
|
462
|
+
async function validateOwnerPasswordAES256(password, encryptParams) {
|
|
463
|
+
const { ownerKey, userKey, ownerEncryptKey, revision } = encryptParams;
|
|
464
|
+
|
|
465
|
+
// O validation salt = O[32:40], userKey = full 48-byte U value
|
|
466
|
+
const validationSalt = ownerKey.slice(32, 40);
|
|
467
|
+
const hash = await aes256PasswordHash(password, validationSalt, userKey, revision);
|
|
468
|
+
|
|
469
|
+
// Compare hash with O[0:32]
|
|
470
|
+
if (!arraysEqual(hash, ownerKey.slice(0, 32))) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Password valid — derive file key from OE
|
|
475
|
+
// Key salt = O[40:48]
|
|
476
|
+
const keySalt = ownerKey.slice(40, 48);
|
|
477
|
+
const oeKey = await aes256PasswordHash(password, keySalt, userKey, revision);
|
|
478
|
+
const zeroIV = new Uint8Array(16);
|
|
479
|
+
const fileKey = await aes256CbcDecryptNoPad(ownerEncryptKey, oeKey, zeroIV);
|
|
480
|
+
|
|
481
|
+
return fileKey;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Verify Perms value (Algorithm 13 / ISO 32000-2)
|
|
486
|
+
* Decrypts /Perms with the file key and checks consistency
|
|
487
|
+
*
|
|
488
|
+
* @returns {boolean} - true if Perms is valid
|
|
489
|
+
*/
|
|
490
|
+
async function verifyPerms(fileKey, encryptParams) {
|
|
491
|
+
const { perms, permissions, encryptMetadata } = encryptParams;
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const decrypted = await aes256EcbDecryptBlock(perms, fileKey);
|
|
495
|
+
|
|
496
|
+
// Bytes 0-3 should match permissions (little-endian)
|
|
497
|
+
const p0 = decrypted[0] | (decrypted[1] << 8) | (decrypted[2] << 16) | (decrypted[3] << 24);
|
|
498
|
+
if ((p0 | 0) !== (permissions | 0)) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Byte 8 should be 'T' (0x54) or 'F' (0x46)
|
|
503
|
+
const expectedEM = encryptMetadata ? 0x54 : 0x46;
|
|
504
|
+
if (decrypted[8] !== expectedEM) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Bytes 9-11 should be 'a', 'd', 'b'
|
|
509
|
+
if (decrypted[9] !== 0x61 || decrypted[10] !== 0x64 || decrypted[11] !== 0x62) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return true;
|
|
514
|
+
} catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ========== AES-256 Object Decryption (Batched) ==========
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Collect all encrypted items from the PDF object tree (synchronous traversal)
|
|
523
|
+
* Returns arrays of items that need AES-256 decryption
|
|
524
|
+
*/
|
|
525
|
+
function collectEncryptedItems(context, encryptRefNum, encryptMetadata) {
|
|
526
|
+
const streamItems = [];
|
|
527
|
+
const stringItems = [];
|
|
528
|
+
const indirectObjects = context.enumerateIndirectObjects();
|
|
529
|
+
|
|
530
|
+
for (const [ref, obj] of indirectObjects) {
|
|
531
|
+
const objectNum = ref.objectNumber;
|
|
532
|
+
const generationNum = ref.generationNumber || 0;
|
|
533
|
+
|
|
534
|
+
// Skip the encryption dictionary itself
|
|
535
|
+
if (encryptRefNum !== null && objectNum === encryptRefNum) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Skip signature dictionaries — /Contents and /ByteRange must not be decrypted
|
|
540
|
+
// per PDF spec Section 7.6.1 (applies to both stream and non-stream /Sig objects)
|
|
541
|
+
if (obj instanceof PDFDict && !(obj instanceof PDFRawStream)) {
|
|
542
|
+
const type = obj.get(PDFName.of('Type'));
|
|
543
|
+
if (type && type.toString() === '/Sig') continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Check if this is a stream with a /Type we should skip
|
|
547
|
+
if (obj instanceof PDFRawStream && obj.dict) {
|
|
548
|
+
const type = obj.dict.get(PDFName.of('Type'));
|
|
549
|
+
if (type) {
|
|
550
|
+
const typeName = type.toString();
|
|
551
|
+
// XRef streams are never encrypted
|
|
552
|
+
if (typeName === '/XRef') continue;
|
|
553
|
+
// Signature streams must not be decrypted
|
|
554
|
+
if (typeName === '/Sig') continue;
|
|
555
|
+
// Skip metadata streams when EncryptMetadata is false
|
|
556
|
+
if (typeName === '/Metadata' && !encryptMetadata) continue;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Collect streams for decryption
|
|
561
|
+
if (obj instanceof PDFRawStream) {
|
|
562
|
+
const streamData = obj.contents;
|
|
563
|
+
// AES-256 streams: IV (16 bytes) + ciphertext
|
|
564
|
+
if (streamData.length >= 16) {
|
|
565
|
+
streamItems.push({ ref, obj, data: streamData, objectNum, generationNum });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Collect strings in stream dictionaries
|
|
569
|
+
if (obj.dict) {
|
|
570
|
+
collectStringsFromObject(obj.dict, objectNum, generationNum, stringItems);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Collect strings in non-stream objects
|
|
575
|
+
if (!(obj instanceof PDFRawStream)) {
|
|
576
|
+
collectStringsFromObject(obj, objectNum, generationNum, stringItems);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return { streamItems, stringItems };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Recursively collect encrypted strings from a PDF object (synchronous)
|
|
585
|
+
*/
|
|
586
|
+
function collectStringsFromObject(obj, objectNum, generationNum, items) {
|
|
587
|
+
if (!obj) return;
|
|
588
|
+
|
|
589
|
+
if (obj instanceof PDFString) {
|
|
590
|
+
const bytes = obj.asBytes();
|
|
591
|
+
if (bytes.length >= 16) {
|
|
592
|
+
items.push({ obj, bytes, type: 'string', objectNum, generationNum });
|
|
593
|
+
}
|
|
594
|
+
} else if (obj instanceof PDFHexString) {
|
|
595
|
+
const bytes = obj.asBytes();
|
|
596
|
+
if (bytes.length >= 16) {
|
|
597
|
+
items.push({ obj, bytes, type: 'hex', objectNum, generationNum });
|
|
598
|
+
}
|
|
599
|
+
} else if (obj instanceof PDFDict) {
|
|
600
|
+
for (const [key, value] of obj.entries()) {
|
|
601
|
+
const keyName = key.asString();
|
|
602
|
+
if (keyName !== '/Length' && keyName !== '/Filter' && keyName !== '/DecodeParms') {
|
|
603
|
+
collectStringsFromObject(value, objectNum, generationNum, items);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} else if (obj instanceof PDFArray) {
|
|
607
|
+
for (const element of obj.asArray()) {
|
|
608
|
+
collectStringsFromObject(element, objectNum, generationNum, items);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Decrypt a single AES-256 encrypted blob (IV prepended)
|
|
615
|
+
* Per PDF 2.0: first 16 bytes are the IV, rest is ciphertext
|
|
616
|
+
*/
|
|
617
|
+
async function decryptAES256Blob(data, cryptoKey) {
|
|
618
|
+
const iv = data.slice(0, 16);
|
|
619
|
+
const ciphertext = data.slice(16);
|
|
620
|
+
|
|
621
|
+
// Edge case: empty ciphertext after IV
|
|
622
|
+
if (ciphertext.length === 0) {
|
|
623
|
+
return new Uint8Array(0);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Ciphertext must be a multiple of 16 bytes for AES-CBC
|
|
627
|
+
if (ciphertext.length % 16 !== 0) {
|
|
628
|
+
return data; // Return original data if not valid AES-CBC
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
return await aes256CbcDecryptWithKey(ciphertext, cryptoKey, iv);
|
|
633
|
+
} catch {
|
|
634
|
+
// If decryption fails (e.g., invalid padding), return original
|
|
635
|
+
return data;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Decrypt all collected items in batches using Promise.all
|
|
641
|
+
* This avoids the microtask queue overhead of awaiting each item individually
|
|
642
|
+
*/
|
|
643
|
+
async function decryptAllAES256(streamItems, stringItems, cryptoKey) {
|
|
644
|
+
// Decrypt streams in batches
|
|
645
|
+
for (let i = 0; i < streamItems.length; i += BATCH_SIZE) {
|
|
646
|
+
const batch = streamItems.slice(i, i + BATCH_SIZE);
|
|
647
|
+
const results = await Promise.all(
|
|
648
|
+
batch.map(item => decryptAES256Blob(item.data, cryptoKey))
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// Write results back synchronously
|
|
652
|
+
for (let j = 0; j < batch.length; j++) {
|
|
653
|
+
batch[j].obj.contents = results[j];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Decrypt strings in batches
|
|
658
|
+
for (let i = 0; i < stringItems.length; i += BATCH_SIZE) {
|
|
659
|
+
const batch = stringItems.slice(i, i + BATCH_SIZE);
|
|
660
|
+
const results = await Promise.all(
|
|
661
|
+
batch.map(item => decryptAES256Blob(item.bytes, cryptoKey))
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Write results back synchronously
|
|
665
|
+
for (let j = 0; j < batch.length; j++) {
|
|
666
|
+
const item = batch[j];
|
|
667
|
+
const decrypted = results[j];
|
|
668
|
+
|
|
669
|
+
if (item.type === 'string') {
|
|
670
|
+
item.obj.value = Array.from(decrypted).map(b => String.fromCharCode(b)).join('');
|
|
671
|
+
} else {
|
|
672
|
+
item.obj.value = bytesToHex(decrypted);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ========== RC4 Decryption Path ==========
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Decrypt all objects using RC4 (synchronous, from decrypt-lite)
|
|
682
|
+
*/
|
|
683
|
+
function decryptAllRC4(context, encryptionKey, encryptRefNum) {
|
|
684
|
+
const indirectObjects = context.enumerateIndirectObjects();
|
|
685
|
+
|
|
686
|
+
for (const [ref, obj] of indirectObjects) {
|
|
687
|
+
const objectNum = ref.objectNumber;
|
|
688
|
+
const generationNum = ref.generationNumber || 0;
|
|
689
|
+
|
|
690
|
+
// Skip the encryption dictionary itself
|
|
691
|
+
if (encryptRefNum !== null && objectNum === encryptRefNum) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Skip objects that must not be decrypted per PDF spec (Section 7.6.1)
|
|
696
|
+
// Signature dictionaries: /Contents and /ByteRange must not be decrypted
|
|
697
|
+
if (obj instanceof PDFDict && !(obj instanceof PDFRawStream)) {
|
|
698
|
+
const type = obj.get(PDFName.of('Type'));
|
|
699
|
+
if (type && type.toString() === '/Sig') continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (obj instanceof PDFRawStream && obj.dict) {
|
|
703
|
+
const type = obj.dict.get(PDFName.of('Type'));
|
|
704
|
+
if (type) {
|
|
705
|
+
const typeName = type.toString();
|
|
706
|
+
if (typeName === '/XRef' || typeName === '/Sig') {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Decrypt streams
|
|
713
|
+
if (obj instanceof PDFRawStream) {
|
|
714
|
+
const streamData = obj.contents;
|
|
715
|
+
const decrypted = decryptObjectRC4(streamData, objectNum, generationNum, encryptionKey);
|
|
716
|
+
obj.contents = decrypted;
|
|
717
|
+
|
|
718
|
+
// Also decrypt strings within the stream's dictionary
|
|
719
|
+
if (obj.dict) {
|
|
720
|
+
decryptStringsRC4(obj.dict, objectNum, generationNum, encryptionKey);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Decrypt strings in non-stream objects
|
|
725
|
+
if (!(obj instanceof PDFRawStream)) {
|
|
726
|
+
decryptStringsRC4(obj, objectNum, generationNum, encryptionKey);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ========== Main API ==========
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Decrypt a password-protected PDF
|
|
735
|
+
*
|
|
736
|
+
* Supports both AES-256 (V=5, R=6) and RC4 (V=1-2, R=2-3) encryption.
|
|
737
|
+
* React Native compatible PDF decryption (fork of @pdfsmaller/pdf-decrypt)
|
|
738
|
+
* https://github.com/imdewan/rn-pdf-decrypt
|
|
739
|
+
*
|
|
740
|
+
* @param {Uint8Array} pdfBytes - The encrypted PDF file as bytes
|
|
741
|
+
* @param {string} password - The user or owner password
|
|
742
|
+
* @returns {Promise<Uint8Array>} - The decrypted PDF bytes
|
|
743
|
+
* @throws {Error} If the PDF is not encrypted, password is wrong, or encryption is unsupported
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* import { decryptPDF } from 'rn-pdf-decrypt';
|
|
747
|
+
*
|
|
748
|
+
* const decrypted = await decryptPDF(encryptedBytes, 'secret123');
|
|
749
|
+
*/
|
|
750
|
+
async function decryptPDF(pdfBytes, password) {
|
|
751
|
+
try {
|
|
752
|
+
// Load the PDF without attempting to decrypt (let us handle it)
|
|
753
|
+
const pdfDoc = await PDFDocument.load(pdfBytes, {
|
|
754
|
+
ignoreEncryption: true,
|
|
755
|
+
updateMetadata: false
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const context = pdfDoc.context;
|
|
759
|
+
|
|
760
|
+
// Read encryption parameters
|
|
761
|
+
const encryptParams = readEncryptParams(context);
|
|
762
|
+
|
|
763
|
+
if (!encryptParams) {
|
|
764
|
+
throw new Error('This PDF is not encrypted. No /Encrypt dictionary found.');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const encryptRefNum = (encryptParams.encryptRef instanceof PDFRef)
|
|
768
|
+
? encryptParams.encryptRef.objectNumber
|
|
769
|
+
: null;
|
|
770
|
+
|
|
771
|
+
if (encryptParams.algorithm === 'AES-256') {
|
|
772
|
+
// ========== AES-256 Path ==========
|
|
773
|
+
const pwdBytes = saslPrepPassword(password);
|
|
774
|
+
|
|
775
|
+
// Try user password first
|
|
776
|
+
let fileKey = await validateUserPasswordAES256(pwdBytes, encryptParams);
|
|
777
|
+
|
|
778
|
+
// If user password fails, try owner password
|
|
779
|
+
if (!fileKey) {
|
|
780
|
+
fileKey = await validateOwnerPasswordAES256(pwdBytes, encryptParams);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (!fileKey) {
|
|
784
|
+
throw new Error('Incorrect password. The provided password does not match the user or owner password.');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Verify Perms (optional but recommended — warns but doesn't fail)
|
|
788
|
+
const permsValid = await verifyPerms(fileKey, encryptParams);
|
|
789
|
+
if (!permsValid) {
|
|
790
|
+
// Some PDFs have invalid Perms but are otherwise fine — proceed anyway
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Import key once for bulk decryption
|
|
794
|
+
const cryptoKey = await importAES256DecryptKey(fileKey);
|
|
795
|
+
|
|
796
|
+
// Collect all encrypted items (synchronous traversal)
|
|
797
|
+
const { streamItems, stringItems } = collectEncryptedItems(
|
|
798
|
+
context, encryptRefNum, encryptParams.encryptMetadata
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// Decrypt all items in batches (async)
|
|
802
|
+
await decryptAllAES256(streamItems, stringItems, cryptoKey);
|
|
803
|
+
|
|
804
|
+
} else {
|
|
805
|
+
// ========== RC4 Path ==========
|
|
806
|
+
// Try user password first
|
|
807
|
+
let encryptionKey = validateUserPasswordRC4(password, encryptParams);
|
|
808
|
+
|
|
809
|
+
if (!encryptionKey) {
|
|
810
|
+
encryptionKey = validateOwnerPasswordRC4(password, encryptParams);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!encryptionKey) {
|
|
814
|
+
throw new Error('Incorrect password. The provided password does not match the user or owner password.');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Decrypt all objects (synchronous)
|
|
818
|
+
decryptAllRC4(context, encryptionKey, encryptRefNum);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Remove the /Encrypt entry from the trailer
|
|
822
|
+
delete context.trailerInfo.Encrypt;
|
|
823
|
+
|
|
824
|
+
// Save the decrypted PDF
|
|
825
|
+
const decryptedBytes = await pdfDoc.save({
|
|
826
|
+
useObjectStreams: false
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
return decryptedBytes;
|
|
830
|
+
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (error.message.includes('not encrypted') ||
|
|
833
|
+
error.message.includes('Incorrect password') ||
|
|
834
|
+
error.message.includes('Unsupported encryption')) {
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
throw new Error(`Failed to decrypt PDF: ${error.message}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Check if a PDF is encrypted (without attempting to decrypt)
|
|
843
|
+
*
|
|
844
|
+
* @param {Uint8Array} pdfBytes - The PDF file as bytes
|
|
845
|
+
* @returns {Promise<{encrypted: boolean, algorithm?: 'AES-256'|'RC4', version?: number, revision?: number, keyLength?: number}>}
|
|
846
|
+
*
|
|
847
|
+
* @example
|
|
848
|
+
* const info = await isEncrypted(pdfBytes);
|
|
849
|
+
* if (info.encrypted) {
|
|
850
|
+
* console.log(`Encrypted with ${info.algorithm}`);
|
|
851
|
+
* }
|
|
852
|
+
*/
|
|
853
|
+
async function isEncrypted(pdfBytes) {
|
|
854
|
+
try {
|
|
855
|
+
const pdfDoc = await PDFDocument.load(pdfBytes, {
|
|
856
|
+
ignoreEncryption: true,
|
|
857
|
+
updateMetadata: false
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const encryptParams = readEncryptParams(pdfDoc.context);
|
|
861
|
+
|
|
862
|
+
if (!encryptParams) {
|
|
863
|
+
return { encrypted: false };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
encrypted: true,
|
|
868
|
+
algorithm: encryptParams.algorithm,
|
|
869
|
+
version: encryptParams.version,
|
|
870
|
+
revision: encryptParams.revision,
|
|
871
|
+
keyLength: encryptParams.keyLength * 8 // return in bits
|
|
872
|
+
};
|
|
873
|
+
} catch (error) {
|
|
874
|
+
throw new Error(`Failed to read PDF: ${error.message}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* rn-pdf-decrypt — https://github.com/imdewan/rn-pdf-decrypt
|
|
880
|
+
* Fork of @pdfsmaller/pdf-decrypt for React Native compatibility
|
|
881
|
+
*/
|
|
882
|
+
|
|
883
|
+
module.exports = { decryptPDF, isEncrypted };
|