@cj-tech-master/excelts 8.0.0 → 8.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/README.md +14 -1
- package/README_zh.md +6 -0
- package/dist/browser/modules/archive/zip/stream.d.ts +4 -0
- package/dist/browser/modules/archive/zip/stream.js +53 -0
- package/dist/browser/modules/pdf/core/crypto.d.ts +65 -0
- package/dist/browser/modules/pdf/core/crypto.js +637 -0
- package/dist/browser/modules/pdf/core/encryption.d.ts +23 -20
- package/dist/browser/modules/pdf/core/encryption.js +88 -261
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +6 -4
- package/dist/browser/modules/pdf/core/pdf-writer.js +19 -10
- package/dist/browser/modules/pdf/index.d.ts +23 -2
- package/dist/browser/modules/pdf/index.js +21 -3
- package/dist/browser/modules/pdf/reader/annotation-extractor.d.ts +63 -0
- package/dist/browser/modules/pdf/reader/annotation-extractor.js +155 -0
- package/dist/browser/modules/pdf/reader/cmap-parser.d.ts +70 -0
- package/dist/browser/modules/pdf/reader/cmap-parser.js +321 -0
- package/dist/browser/modules/pdf/reader/content-interpreter.d.ts +57 -0
- package/dist/browser/modules/pdf/reader/content-interpreter.js +715 -0
- package/dist/browser/modules/pdf/reader/font-decoder.d.ts +58 -0
- package/dist/browser/modules/pdf/reader/font-decoder.js +1513 -0
- package/dist/browser/modules/pdf/reader/form-extractor.d.ts +48 -0
- package/dist/browser/modules/pdf/reader/form-extractor.js +355 -0
- package/dist/browser/modules/pdf/reader/image-extractor.d.ts +55 -0
- package/dist/browser/modules/pdf/reader/image-extractor.js +220 -0
- package/dist/browser/modules/pdf/reader/metadata-reader.d.ts +56 -0
- package/dist/browser/modules/pdf/reader/metadata-reader.js +275 -0
- package/dist/browser/modules/pdf/reader/pdf-decrypt.d.ts +26 -0
- package/dist/browser/modules/pdf/reader/pdf-decrypt.js +443 -0
- package/dist/browser/modules/pdf/reader/pdf-document.d.ts +191 -0
- package/dist/browser/modules/pdf/reader/pdf-document.js +818 -0
- package/dist/browser/modules/pdf/reader/pdf-parser.d.ts +65 -0
- package/dist/browser/modules/pdf/reader/pdf-parser.js +285 -0
- package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +143 -0
- package/dist/browser/modules/pdf/reader/pdf-reader.js +200 -0
- package/dist/browser/modules/pdf/reader/pdf-tokenizer.d.ts +101 -0
- package/dist/browser/modules/pdf/reader/pdf-tokenizer.js +543 -0
- package/dist/browser/modules/pdf/reader/reader-utils.d.ts +15 -0
- package/dist/browser/modules/pdf/reader/reader-utils.js +27 -0
- package/dist/browser/modules/pdf/reader/stream-filters.d.ts +20 -0
- package/dist/browser/modules/pdf/reader/stream-filters.js +456 -0
- package/dist/browser/modules/pdf/reader/text-reconstruction.d.ts +44 -0
- package/dist/browser/modules/pdf/reader/text-reconstruction.js +463 -0
- package/dist/cjs/modules/archive/zip/stream.js +53 -0
- package/dist/cjs/modules/pdf/core/crypto.js +649 -0
- package/dist/cjs/modules/pdf/core/encryption.js +88 -263
- package/dist/cjs/modules/pdf/core/pdf-writer.js +19 -10
- package/dist/cjs/modules/pdf/index.js +23 -4
- package/dist/cjs/modules/pdf/reader/annotation-extractor.js +158 -0
- package/dist/cjs/modules/pdf/reader/cmap-parser.js +326 -0
- package/dist/cjs/modules/pdf/reader/content-interpreter.js +718 -0
- package/dist/cjs/modules/pdf/reader/font-decoder.js +1518 -0
- package/dist/cjs/modules/pdf/reader/form-extractor.js +358 -0
- package/dist/cjs/modules/pdf/reader/image-extractor.js +223 -0
- package/dist/cjs/modules/pdf/reader/metadata-reader.js +278 -0
- package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +447 -0
- package/dist/cjs/modules/pdf/reader/pdf-document.js +822 -0
- package/dist/cjs/modules/pdf/reader/pdf-parser.js +301 -0
- package/dist/cjs/modules/pdf/reader/pdf-reader.js +203 -0
- package/dist/cjs/modules/pdf/reader/pdf-tokenizer.js +517 -0
- package/dist/cjs/modules/pdf/reader/reader-utils.js +30 -0
- package/dist/cjs/modules/pdf/reader/stream-filters.js +459 -0
- package/dist/cjs/modules/pdf/reader/text-reconstruction.js +467 -0
- package/dist/esm/modules/archive/zip/stream.js +53 -0
- package/dist/esm/modules/pdf/core/crypto.js +637 -0
- package/dist/esm/modules/pdf/core/encryption.js +88 -261
- package/dist/esm/modules/pdf/core/pdf-writer.js +19 -10
- package/dist/esm/modules/pdf/index.js +21 -3
- package/dist/esm/modules/pdf/reader/annotation-extractor.js +155 -0
- package/dist/esm/modules/pdf/reader/cmap-parser.js +321 -0
- package/dist/esm/modules/pdf/reader/content-interpreter.js +715 -0
- package/dist/esm/modules/pdf/reader/font-decoder.js +1513 -0
- package/dist/esm/modules/pdf/reader/form-extractor.js +355 -0
- package/dist/esm/modules/pdf/reader/image-extractor.js +220 -0
- package/dist/esm/modules/pdf/reader/metadata-reader.js +275 -0
- package/dist/esm/modules/pdf/reader/pdf-decrypt.js +443 -0
- package/dist/esm/modules/pdf/reader/pdf-document.js +818 -0
- package/dist/esm/modules/pdf/reader/pdf-parser.js +285 -0
- package/dist/esm/modules/pdf/reader/pdf-reader.js +200 -0
- package/dist/esm/modules/pdf/reader/pdf-tokenizer.js +543 -0
- package/dist/esm/modules/pdf/reader/reader-utils.js +27 -0
- package/dist/esm/modules/pdf/reader/stream-filters.js +456 -0
- package/dist/esm/modules/pdf/reader/text-reconstruction.js +463 -0
- package/dist/iife/excelts.iife.js +703 -267
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +35 -35
- package/dist/types/modules/archive/zip/stream.d.ts +4 -0
- package/dist/types/modules/pdf/core/crypto.d.ts +65 -0
- package/dist/types/modules/pdf/core/encryption.d.ts +23 -20
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +6 -4
- package/dist/types/modules/pdf/index.d.ts +23 -2
- package/dist/types/modules/pdf/reader/annotation-extractor.d.ts +63 -0
- package/dist/types/modules/pdf/reader/cmap-parser.d.ts +70 -0
- package/dist/types/modules/pdf/reader/content-interpreter.d.ts +57 -0
- package/dist/types/modules/pdf/reader/font-decoder.d.ts +58 -0
- package/dist/types/modules/pdf/reader/form-extractor.d.ts +48 -0
- package/dist/types/modules/pdf/reader/image-extractor.d.ts +55 -0
- package/dist/types/modules/pdf/reader/metadata-reader.d.ts +56 -0
- package/dist/types/modules/pdf/reader/pdf-decrypt.d.ts +26 -0
- package/dist/types/modules/pdf/reader/pdf-document.d.ts +191 -0
- package/dist/types/modules/pdf/reader/pdf-parser.d.ts +65 -0
- package/dist/types/modules/pdf/reader/pdf-reader.d.ts +143 -0
- package/dist/types/modules/pdf/reader/pdf-tokenizer.d.ts +101 -0
- package/dist/types/modules/pdf/reader/reader-utils.d.ts +15 -0
- package/dist/types/modules/pdf/reader/stream-filters.d.ts +20 -0
- package/dist/types/modules/pdf/reader/text-reconstruction.d.ts +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PDF decryption for reading encrypted PDFs.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Standard Security Handler (V1/V2/V4/V5, R2/R3/R4/R5)
|
|
7
|
+
* - RC4 encryption (40-bit and 128-bit)
|
|
8
|
+
* - AES-128 encryption (PDF 1.6+)
|
|
9
|
+
* - AES-256 encryption (PDF 2.0, V=5, R=5)
|
|
10
|
+
*
|
|
11
|
+
* @see PDF Reference 1.7, §3.5 - Encryption
|
|
12
|
+
* @see PDF 2.0 (ISO 32000-2), §7.6 - Encryption
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.initDecryption = initDecryption;
|
|
16
|
+
exports.isEncrypted = isEncrypted;
|
|
17
|
+
const crypto_1 = require("../core/crypto");
|
|
18
|
+
const pdf_parser_1 = require("./pdf-parser");
|
|
19
|
+
const errors_1 = require("../errors");
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Constants
|
|
22
|
+
// =============================================================================
|
|
23
|
+
/** PDF password padding string (32 bytes) per PDF spec §3.5.2 */
|
|
24
|
+
const PASSWORD_PADDING = new Uint8Array([
|
|
25
|
+
0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08,
|
|
26
|
+
0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a
|
|
27
|
+
]);
|
|
28
|
+
/** Cached TextEncoder instance */
|
|
29
|
+
const textEncoder = new TextEncoder();
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Public API
|
|
32
|
+
// =============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Initialize decryption for a PDF document.
|
|
35
|
+
* Returns true if decryption was successfully initialized, false if
|
|
36
|
+
* the password was incorrect.
|
|
37
|
+
*
|
|
38
|
+
* @param doc - The PDF document
|
|
39
|
+
* @param password - User or owner password (empty string for no password)
|
|
40
|
+
*/
|
|
41
|
+
function initDecryption(doc, password = "") {
|
|
42
|
+
const encryptDict = doc.derefDict(doc.trailer.get("Encrypt"));
|
|
43
|
+
if (!encryptDict) {
|
|
44
|
+
return true; // Not encrypted
|
|
45
|
+
}
|
|
46
|
+
const filter = (0, pdf_parser_1.dictGetName)(encryptDict, "Filter");
|
|
47
|
+
if (filter !== "Standard") {
|
|
48
|
+
throw new errors_1.PdfStructureError(`Unsupported encryption filter: ${filter}`);
|
|
49
|
+
}
|
|
50
|
+
const v = (0, pdf_parser_1.dictGetNumber)(encryptDict, "V") ?? 0;
|
|
51
|
+
const r = (0, pdf_parser_1.dictGetNumber)(encryptDict, "R") ?? 0;
|
|
52
|
+
const keyLength = ((0, pdf_parser_1.dictGetNumber)(encryptDict, "Length") ?? 40) / 8; // bits → bytes
|
|
53
|
+
const permissions = (0, pdf_parser_1.dictGetNumber)(encryptDict, "P") ?? 0;
|
|
54
|
+
const oValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "O");
|
|
55
|
+
const uValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "U");
|
|
56
|
+
if (!oValue || !uValue) {
|
|
57
|
+
throw new errors_1.PdfStructureError("Missing /O or /U values in Encrypt dictionary");
|
|
58
|
+
}
|
|
59
|
+
// Get file ID from trailer
|
|
60
|
+
const idArray = (0, pdf_parser_1.dictGetArray)(doc.trailer, "ID");
|
|
61
|
+
const fileId = idArray && idArray.length > 0 && idArray[0] instanceof Uint8Array
|
|
62
|
+
? idArray[0]
|
|
63
|
+
: new Uint8Array(0);
|
|
64
|
+
// Determine EncryptMetadata flag (default true per spec)
|
|
65
|
+
const encryptMetadata = readEncryptMetadata(encryptDict);
|
|
66
|
+
// Handle V=5 (AES-256, PDF 2.0)
|
|
67
|
+
if (v === 5) {
|
|
68
|
+
return initDecryptionV5(doc, encryptDict, password, r, oValue, uValue, permissions, fileId);
|
|
69
|
+
}
|
|
70
|
+
// Determine if we should use AES
|
|
71
|
+
const useAes = v === 4 && isAesCryptFilter(encryptDict);
|
|
72
|
+
// Try user password first, then owner password
|
|
73
|
+
let encryptionKey = tryUserPassword(password, oValue, permissions, fileId, r, keyLength, uValue, encryptMetadata);
|
|
74
|
+
if (!encryptionKey) {
|
|
75
|
+
// Try as owner password
|
|
76
|
+
const derivedUser = deriveUserPasswordFromOwner(password, oValue, r, keyLength);
|
|
77
|
+
encryptionKey = tryUserPassword(derivedUser, oValue, permissions, fileId, r, keyLength, uValue, encryptMetadata);
|
|
78
|
+
}
|
|
79
|
+
if (!encryptionKey) {
|
|
80
|
+
// Try empty password
|
|
81
|
+
if (password !== "") {
|
|
82
|
+
encryptionKey = tryUserPassword("", oValue, permissions, fileId, r, keyLength, uValue, encryptMetadata);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!encryptionKey) {
|
|
86
|
+
return false; // Password incorrect
|
|
87
|
+
}
|
|
88
|
+
// Set up decryption function
|
|
89
|
+
const finalKey = encryptionKey;
|
|
90
|
+
if (useAes) {
|
|
91
|
+
doc.decryptFn = (data, objNum, gen) => decryptAes128(data, objNum, gen, finalKey);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
doc.decryptFn = (data, objNum, gen) => decryptRc4PerObject(data, objNum, gen, finalKey);
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if the document is encrypted.
|
|
100
|
+
*/
|
|
101
|
+
function isEncrypted(doc) {
|
|
102
|
+
return doc.trailer.has("Encrypt");
|
|
103
|
+
}
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// V5 (AES-256) Decryption
|
|
106
|
+
// =============================================================================
|
|
107
|
+
/**
|
|
108
|
+
* Initialize decryption for V=5 (AES-256, PDF 2.0).
|
|
109
|
+
* Supports R=5 using SHA-256 based key derivation (Algorithm 2.A).
|
|
110
|
+
*/
|
|
111
|
+
function initDecryptionV5(doc, encryptDict, password, revision, oValue, uValue, _permissions, _fileId) {
|
|
112
|
+
if (revision === 6) {
|
|
113
|
+
throw new errors_1.PdfStructureError("R=6 (PDF 2.0 extension) requires SHA-384/SHA-512 which is not yet supported");
|
|
114
|
+
}
|
|
115
|
+
if (revision !== 5) {
|
|
116
|
+
throw new errors_1.PdfStructureError(`Unsupported revision ${revision} for V=5 encryption`);
|
|
117
|
+
}
|
|
118
|
+
const oeValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "OE");
|
|
119
|
+
const ueValue = (0, pdf_parser_1.dictGetBytes)(encryptDict, "UE");
|
|
120
|
+
if (!oeValue || !ueValue) {
|
|
121
|
+
throw new errors_1.PdfStructureError("Missing /OE or /UE values in V=5 Encrypt dictionary");
|
|
122
|
+
}
|
|
123
|
+
// O value layout: 32 bytes hash + 8 bytes validation salt + 8 bytes key salt
|
|
124
|
+
// U value layout: 32 bytes hash + 8 bytes validation salt + 8 bytes key salt
|
|
125
|
+
if (oValue.length < 48 || uValue.length < 48) {
|
|
126
|
+
throw new errors_1.PdfStructureError("Invalid /O or /U length for V=5 encryption");
|
|
127
|
+
}
|
|
128
|
+
const passwordBytes = truncatePassword(password);
|
|
129
|
+
// Try user password (Algorithm 2.A step a - user)
|
|
130
|
+
let encryptionKey = tryUserPasswordV5(passwordBytes, uValue, ueValue);
|
|
131
|
+
if (!encryptionKey) {
|
|
132
|
+
// Try owner password (Algorithm 2.A step a - owner)
|
|
133
|
+
encryptionKey = tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue);
|
|
134
|
+
}
|
|
135
|
+
if (!encryptionKey) {
|
|
136
|
+
// Try empty password
|
|
137
|
+
if (password !== "") {
|
|
138
|
+
const emptyBytes = new Uint8Array(0);
|
|
139
|
+
encryptionKey = tryUserPasswordV5(emptyBytes, uValue, ueValue);
|
|
140
|
+
if (!encryptionKey) {
|
|
141
|
+
encryptionKey = tryOwnerPasswordV5(emptyBytes, oValue, oeValue, uValue);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!encryptionKey) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
// V=5 always uses AES-256 with the file encryption key directly (no per-object key derivation)
|
|
149
|
+
const finalKey = encryptionKey;
|
|
150
|
+
doc.decryptFn = (data, _objNum, _gen) => decryptAes256Direct(data, finalKey);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Truncate password to 127 bytes (UTF-8) per PDF 2.0 spec.
|
|
155
|
+
*/
|
|
156
|
+
function truncatePassword(password) {
|
|
157
|
+
const bytes = textEncoder.encode(password);
|
|
158
|
+
return bytes.length > 127 ? bytes.subarray(0, 127) : bytes;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Try user password for V=5/R=5.
|
|
162
|
+
* Validates using SHA-256(password + validation salt from U).
|
|
163
|
+
* If valid, derives file encryption key using SHA-256(password + key salt from U).
|
|
164
|
+
*/
|
|
165
|
+
function tryUserPasswordV5(passwordBytes, uValue, ueValue) {
|
|
166
|
+
// U = hash(32) + validation salt(8) + key salt(8)
|
|
167
|
+
const uHash = uValue.subarray(0, 32);
|
|
168
|
+
const uValidationSalt = uValue.subarray(32, 40);
|
|
169
|
+
const uKeySalt = uValue.subarray(40, 48);
|
|
170
|
+
// Validate: SHA-256(password + validation salt) == first 32 bytes of U
|
|
171
|
+
const validateInput = (0, crypto_1.concatArrays)(passwordBytes, uValidationSalt);
|
|
172
|
+
const computedHash = (0, crypto_1.sha256)(validateInput);
|
|
173
|
+
if (!arraysEqual(computedHash, uHash)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
// Derive key: SHA-256(password + key salt) => use as AES-256 key to decrypt UE
|
|
177
|
+
const keyInput = (0, crypto_1.concatArrays)(passwordBytes, uKeySalt);
|
|
178
|
+
const keyHash = (0, crypto_1.sha256)(keyInput);
|
|
179
|
+
// Decrypt UE with this key using AES-256-CBC with zero IV
|
|
180
|
+
const zeroIv = new Uint8Array(16);
|
|
181
|
+
return (0, crypto_1.aesCbcDecryptRaw)(ueValue.subarray(0, 32), keyHash, zeroIv);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Try owner password for V=5/R=5.
|
|
185
|
+
* Validates using SHA-256(password + validation salt from O + U(48)).
|
|
186
|
+
* If valid, derives file encryption key using SHA-256(password + key salt from O + U(48)).
|
|
187
|
+
*/
|
|
188
|
+
function tryOwnerPasswordV5(passwordBytes, oValue, oeValue, uValue) {
|
|
189
|
+
// O = hash(32) + validation salt(8) + key salt(8)
|
|
190
|
+
const oHash = oValue.subarray(0, 32);
|
|
191
|
+
const oValidationSalt = oValue.subarray(32, 40);
|
|
192
|
+
const oKeySalt = oValue.subarray(40, 48);
|
|
193
|
+
const u48 = uValue.subarray(0, 48);
|
|
194
|
+
// Validate: SHA-256(password + validation salt + U(0..47)) == first 32 bytes of O
|
|
195
|
+
const validateInput = (0, crypto_1.concatArrays)(passwordBytes, oValidationSalt, u48);
|
|
196
|
+
const computedHash = (0, crypto_1.sha256)(validateInput);
|
|
197
|
+
if (!arraysEqual(computedHash, oHash)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// Derive key: SHA-256(password + key salt + U(0..47))
|
|
201
|
+
const keyInput = (0, crypto_1.concatArrays)(passwordBytes, oKeySalt, u48);
|
|
202
|
+
const keyHash = (0, crypto_1.sha256)(keyInput);
|
|
203
|
+
// Decrypt OE with this key using AES-256-CBC with zero IV
|
|
204
|
+
const zeroIv = new Uint8Array(16);
|
|
205
|
+
return (0, crypto_1.aesCbcDecryptRaw)(oeValue.subarray(0, 32), keyHash, zeroIv);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Decrypt data using AES-256 directly (no per-object key derivation).
|
|
209
|
+
* For V=5, the file encryption key is used directly. The first 16 bytes are IV.
|
|
210
|
+
*/
|
|
211
|
+
function decryptAes256Direct(data, encryptionKey) {
|
|
212
|
+
if (data.length < 16) {
|
|
213
|
+
return data;
|
|
214
|
+
}
|
|
215
|
+
const iv = data.subarray(0, 16);
|
|
216
|
+
const ciphertext = data.subarray(16);
|
|
217
|
+
if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
|
|
218
|
+
return data;
|
|
219
|
+
}
|
|
220
|
+
return (0, crypto_1.aesCbcDecrypt)(ciphertext, encryptionKey, iv);
|
|
221
|
+
}
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// Password Verification (V1-V4)
|
|
224
|
+
// =============================================================================
|
|
225
|
+
/**
|
|
226
|
+
* Try to authenticate with the user password.
|
|
227
|
+
* Returns the encryption key if successful, null otherwise.
|
|
228
|
+
*/
|
|
229
|
+
function tryUserPassword(password, oValue, permissions, fileId, revision, keyLength, uValue, encryptMetadata) {
|
|
230
|
+
const key = computeEncryptionKeyForReading(password, oValue, permissions, fileId, revision, keyLength, encryptMetadata);
|
|
231
|
+
// Verify against U value
|
|
232
|
+
if (revision === 2) {
|
|
233
|
+
// R2: encrypt password padding with key, compare to U
|
|
234
|
+
const encrypted = (0, crypto_1.rc4)(key, PASSWORD_PADDING);
|
|
235
|
+
if (arraysEqual(encrypted, uValue.subarray(0, 32))) {
|
|
236
|
+
return key;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (revision >= 3) {
|
|
240
|
+
// R3/R4: MD5(padding + fileId), encrypt, iterate 19 times
|
|
241
|
+
const hashInput = new Uint8Array(32 + fileId.length);
|
|
242
|
+
hashInput.set(PASSWORD_PADDING);
|
|
243
|
+
hashInput.set(fileId, 32);
|
|
244
|
+
const hash = (0, crypto_1.md5)(hashInput);
|
|
245
|
+
let result = (0, crypto_1.rc4)(key, hash);
|
|
246
|
+
for (let i = 1; i <= 19; i++) {
|
|
247
|
+
const modKey = new Uint8Array(key.length);
|
|
248
|
+
for (let j = 0; j < key.length; j++) {
|
|
249
|
+
modKey[j] = key[j] ^ i;
|
|
250
|
+
}
|
|
251
|
+
result = (0, crypto_1.rc4)(modKey, result);
|
|
252
|
+
}
|
|
253
|
+
// Compare first 16 bytes
|
|
254
|
+
if (arraysEqual(result.subarray(0, 16), uValue.subarray(0, 16))) {
|
|
255
|
+
return key;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Compute the encryption key for reading (Algorithm 2, PDF spec §3.5.2).
|
|
262
|
+
*/
|
|
263
|
+
function computeEncryptionKeyForReading(password, oValue, permissions, fileId, revision, keyLength, encryptMetadata) {
|
|
264
|
+
const paddedPwd = padPassword(password);
|
|
265
|
+
// When encryptMetadata is false and revision >= 4, append 4 bytes of 0xFF
|
|
266
|
+
const extraBytes = revision >= 4 && !encryptMetadata ? 4 : 0;
|
|
267
|
+
const input = new Uint8Array(32 + 32 + 4 + fileId.length + extraBytes);
|
|
268
|
+
let offset = 0;
|
|
269
|
+
input.set(paddedPwd, offset);
|
|
270
|
+
offset += 32;
|
|
271
|
+
input.set(oValue.subarray(0, 32), offset);
|
|
272
|
+
offset += 32;
|
|
273
|
+
// P value as 4 LE bytes
|
|
274
|
+
input[offset] = permissions & 0xff;
|
|
275
|
+
input[offset + 1] = (permissions >> 8) & 0xff;
|
|
276
|
+
input[offset + 2] = (permissions >> 16) & 0xff;
|
|
277
|
+
input[offset + 3] = (permissions >> 24) & 0xff;
|
|
278
|
+
offset += 4;
|
|
279
|
+
input.set(fileId, offset);
|
|
280
|
+
offset += fileId.length;
|
|
281
|
+
// If EncryptMetadata is false and revision >= 4, append 0xFFFFFFFF
|
|
282
|
+
if (revision >= 4 && !encryptMetadata) {
|
|
283
|
+
input[offset] = 0xff;
|
|
284
|
+
input[offset + 1] = 0xff;
|
|
285
|
+
input[offset + 2] = 0xff;
|
|
286
|
+
input[offset + 3] = 0xff;
|
|
287
|
+
offset += 4;
|
|
288
|
+
}
|
|
289
|
+
let hash = (0, crypto_1.md5)(input.subarray(0, offset));
|
|
290
|
+
// For revision >= 3, hash 50 more times
|
|
291
|
+
if (revision >= 3) {
|
|
292
|
+
for (let i = 0; i < 50; i++) {
|
|
293
|
+
hash = (0, crypto_1.md5)(hash.subarray(0, keyLength));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return hash.subarray(0, keyLength);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Derive the user password from the owner password.
|
|
300
|
+
* Uses Algorithm 7 from PDF spec §3.5.2.
|
|
301
|
+
*/
|
|
302
|
+
function deriveUserPasswordFromOwner(ownerPassword, oValue, revision, keyLength) {
|
|
303
|
+
let hash = (0, crypto_1.md5)(padPassword(ownerPassword));
|
|
304
|
+
if (revision >= 3) {
|
|
305
|
+
for (let i = 0; i < 50; i++) {
|
|
306
|
+
hash = (0, crypto_1.md5)(hash.subarray(0, keyLength));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const key = hash.subarray(0, keyLength);
|
|
310
|
+
let result = new Uint8Array(oValue.subarray(0, 32));
|
|
311
|
+
if (revision === 2) {
|
|
312
|
+
result = (0, crypto_1.rc4)(key, result);
|
|
313
|
+
}
|
|
314
|
+
else if (revision >= 3) {
|
|
315
|
+
for (let i = 19; i >= 0; i--) {
|
|
316
|
+
const modKey = new Uint8Array(key.length);
|
|
317
|
+
for (let j = 0; j < key.length; j++) {
|
|
318
|
+
modKey[j] = key[j] ^ i;
|
|
319
|
+
}
|
|
320
|
+
result = (0, crypto_1.rc4)(modKey, result);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Convert result bytes to password string
|
|
324
|
+
let pwd = "";
|
|
325
|
+
for (let i = 0; i < 32; i++) {
|
|
326
|
+
if (result[i] === PASSWORD_PADDING[0] &&
|
|
327
|
+
arraysEqual(result.subarray(i, i + Math.min(32 - i, 32)), PASSWORD_PADDING.subarray(0, Math.min(32 - i, 32)))) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
pwd += String.fromCharCode(result[i]);
|
|
331
|
+
}
|
|
332
|
+
return pwd;
|
|
333
|
+
}
|
|
334
|
+
// =============================================================================
|
|
335
|
+
// AES-128 Decryption
|
|
336
|
+
// =============================================================================
|
|
337
|
+
/**
|
|
338
|
+
* Decrypt data using RC4 with per-object key derivation.
|
|
339
|
+
* Per-object key = MD5(encryptionKey + objNum(3LE) + genNum(2LE)), truncated to min(n+5, 16).
|
|
340
|
+
*/
|
|
341
|
+
function decryptRc4PerObject(data, objectNumber, generation, encryptionKey) {
|
|
342
|
+
const keyInput = new Uint8Array(encryptionKey.length + 5);
|
|
343
|
+
keyInput.set(encryptionKey);
|
|
344
|
+
keyInput[encryptionKey.length] = objectNumber & 0xff;
|
|
345
|
+
keyInput[encryptionKey.length + 1] = (objectNumber >> 8) & 0xff;
|
|
346
|
+
keyInput[encryptionKey.length + 2] = (objectNumber >> 16) & 0xff;
|
|
347
|
+
keyInput[encryptionKey.length + 3] = generation & 0xff;
|
|
348
|
+
keyInput[encryptionKey.length + 4] = (generation >> 8) & 0xff;
|
|
349
|
+
const objKey = (0, crypto_1.md5)(keyInput);
|
|
350
|
+
const keyLen = Math.min(encryptionKey.length + 5, 16);
|
|
351
|
+
return (0, crypto_1.rc4)(objKey.subarray(0, keyLen), data);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Decrypt data using AES-128-CBC.
|
|
355
|
+
* Per PDF spec, the first 16 bytes of the data are the IV.
|
|
356
|
+
*/
|
|
357
|
+
function decryptAes128(data, objectNumber, generation, encryptionKey) {
|
|
358
|
+
if (data.length < 16) {
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
// Compute per-object key: MD5(encryptionKey + objNum(3LE) + genNum(2LE) + "sAlT")
|
|
362
|
+
const keyInput = new Uint8Array(encryptionKey.length + 5 + 4);
|
|
363
|
+
keyInput.set(encryptionKey);
|
|
364
|
+
keyInput[encryptionKey.length] = objectNumber & 0xff;
|
|
365
|
+
keyInput[encryptionKey.length + 1] = (objectNumber >> 8) & 0xff;
|
|
366
|
+
keyInput[encryptionKey.length + 2] = (objectNumber >> 16) & 0xff;
|
|
367
|
+
keyInput[encryptionKey.length + 3] = generation & 0xff;
|
|
368
|
+
keyInput[encryptionKey.length + 4] = (generation >> 8) & 0xff;
|
|
369
|
+
// AES salt
|
|
370
|
+
keyInput[encryptionKey.length + 5] = 0x73; // s
|
|
371
|
+
keyInput[encryptionKey.length + 6] = 0x41; // A
|
|
372
|
+
keyInput[encryptionKey.length + 7] = 0x6c; // l
|
|
373
|
+
keyInput[encryptionKey.length + 8] = 0x54; // T
|
|
374
|
+
const objKey = (0, crypto_1.md5)(keyInput);
|
|
375
|
+
const keyLen = Math.min(encryptionKey.length + 5, 16);
|
|
376
|
+
const aesKey = objKey.subarray(0, keyLen);
|
|
377
|
+
// Extract IV (first 16 bytes) and ciphertext
|
|
378
|
+
const iv = data.subarray(0, 16);
|
|
379
|
+
const ciphertext = data.subarray(16);
|
|
380
|
+
if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
|
|
381
|
+
return data;
|
|
382
|
+
}
|
|
383
|
+
return (0, crypto_1.aesCbcDecrypt)(ciphertext, aesKey, iv);
|
|
384
|
+
}
|
|
385
|
+
// =============================================================================
|
|
386
|
+
// Helpers
|
|
387
|
+
// =============================================================================
|
|
388
|
+
function padPassword(password) {
|
|
389
|
+
const result = new Uint8Array(32);
|
|
390
|
+
const bytes = textEncoder.encode(password);
|
|
391
|
+
const len = Math.min(bytes.length, 32);
|
|
392
|
+
result.set(bytes.subarray(0, len));
|
|
393
|
+
result.set(PASSWORD_PADDING.subarray(0, 32 - len), len);
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
function arraysEqual(a, b) {
|
|
397
|
+
if (a.length !== b.length) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
for (let i = 0; i < a.length; i++) {
|
|
401
|
+
if (a[i] !== b[i]) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Read the EncryptMetadata flag from the encrypt dictionary.
|
|
409
|
+
* Per spec, defaults to true if not present.
|
|
410
|
+
* Checks both the top-level dict and CF/StdCF sub-dictionary.
|
|
411
|
+
*/
|
|
412
|
+
function readEncryptMetadata(encryptDict) {
|
|
413
|
+
// Check top-level EncryptMetadata first
|
|
414
|
+
const topLevel = (0, pdf_parser_1.dictGetBool)(encryptDict, "EncryptMetadata");
|
|
415
|
+
if (topLevel !== undefined) {
|
|
416
|
+
return topLevel;
|
|
417
|
+
}
|
|
418
|
+
// Check CF/StdCF/EncryptMetadata
|
|
419
|
+
const cf = encryptDict.get("CF");
|
|
420
|
+
if (cf && cf instanceof Map) {
|
|
421
|
+
const stdCF = cf.get("StdCF");
|
|
422
|
+
if (stdCF && stdCF instanceof Map) {
|
|
423
|
+
const cfVal = stdCF.get("EncryptMetadata");
|
|
424
|
+
if (typeof cfVal === "boolean") {
|
|
425
|
+
return cfVal;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Default per spec
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Check if V4 encryption uses AES (vs RC4).
|
|
434
|
+
*/
|
|
435
|
+
function isAesCryptFilter(encryptDict) {
|
|
436
|
+
const cf = encryptDict.get("CF");
|
|
437
|
+
if (!cf || !(cf instanceof Map)) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
// Check StdCF filter
|
|
441
|
+
const stdCF = cf.get("StdCF");
|
|
442
|
+
if (!stdCF || !(stdCF instanceof Map)) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const cfm = stdCF.get("CFM");
|
|
446
|
+
return cfm === "AESV2";
|
|
447
|
+
}
|