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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 PDFSmaller.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # rn-pdf-decrypt
2
+
3
+ React Native compatible PDF decryption with **AES-256** and **RC4** support. Fork of [@pdfsmaller/pdf-decrypt](https://www.npmjs.com/package/@pdfsmaller/pdf-decrypt) that replaces Web Crypto API with [@noble/hashes](https://www.npmjs.com/package/@noble/hashes) + [@noble/ciphers](https://www.npmjs.com/package/@noble/ciphers) for Hermes compatibility.
4
+
5
+ ## Why this fork?
6
+
7
+ The original `@pdfsmaller/pdf-decrypt` uses `crypto.subtle` (Web Crypto API), which is not available in React Native's Hermes engine. This fork swaps in pure JS crypto from the audited `@noble` libraries, making it work in React Native, Hermes, browsers, Node.js 18+, and Deno.
8
+
9
+ Additionally, this fork adds support for **AES-256 V=5/R=5** (Adobe's pre-ISO extension used by Acrobat X/XI), which the upstream package does not support.
10
+
11
+ ## Features
12
+
13
+ - **AES-256 decryption** (V=5, R=5/6) — PDF 2.0 standard + Adobe extension
14
+ - **RC4 40/128-bit decryption** (V=1-2, R=2-3) — legacy support
15
+ - **User + Owner passwords** — accepts either password to decrypt
16
+ - **React Native / Hermes compatible** — no Web Crypto API dependency
17
+ - **Pure JS crypto** — `@noble/hashes` + `@noble/ciphers` (audited, zero-dep)
18
+ - **Lightweight** — ~18KB total (crypto + decryption logic)
19
+ - **TypeScript types** included
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install rn-pdf-decrypt pdf-lib
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```javascript
30
+ import { decryptPDF } from 'rn-pdf-decrypt';
31
+ import fs from 'fs';
32
+
33
+ const pdfBytes = fs.readFileSync('encrypted.pdf');
34
+ const decrypted = await decryptPDF(new Uint8Array(pdfBytes), 'my-password');
35
+ fs.writeFileSync('decrypted.pdf', decrypted);
36
+ ```
37
+
38
+ ## API
39
+
40
+ ### `decryptPDF(pdfBytes, password)`
41
+
42
+ Decrypt a password-protected PDF. Supports both AES-256 and RC4 encryption — the algorithm is detected automatically.
43
+
44
+ | Parameter | Type | Description |
45
+ |-----------|------|-------------|
46
+ | `pdfBytes` | `Uint8Array` | The encrypted PDF file as bytes |
47
+ | `password` | `string` | The user or owner password |
48
+
49
+ **Returns:** `Promise<Uint8Array>` — The decrypted PDF bytes
50
+
51
+ **Throws:**
52
+ - `"This PDF is not encrypted"` — if the PDF has no encryption dictionary
53
+ - `"Incorrect password"` — if neither user nor owner password matches
54
+ - `"Unsupported encryption"` — if the encryption version is not supported
55
+
56
+ ### `isEncrypted(pdfBytes)`
57
+
58
+ Check if a PDF is encrypted without attempting to decrypt it.
59
+
60
+ | Parameter | Type | Description |
61
+ |-----------|------|-------------|
62
+ | `pdfBytes` | `Uint8Array` | The PDF file as bytes |
63
+
64
+ **Returns:** `Promise<{ encrypted: boolean, algorithm?: 'AES-256' | 'RC4', version?: number, revision?: number, keyLength?: number }>`
65
+
66
+ ## Examples
67
+
68
+ ### Decrypt with Auto-Detection
69
+
70
+ ```javascript
71
+ import { decryptPDF, isEncrypted } from 'rn-pdf-decrypt';
72
+
73
+ // Check encryption type first
74
+ const info = await isEncrypted(pdfBytes);
75
+ if (info.encrypted) {
76
+ console.log(`Encrypted with ${info.algorithm}`);
77
+ const decrypted = await decryptPDF(pdfBytes, password);
78
+ }
79
+ ```
80
+
81
+ ### React Native Usage
82
+
83
+ ```javascript
84
+ import { decryptPDF } from 'rn-pdf-decrypt';
85
+ import RNFS from 'react-native-fs';
86
+
87
+ const base64 = await RNFS.readFile(filePath, 'base64');
88
+ const pdfBytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
89
+
90
+ const decrypted = await decryptPDF(pdfBytes, 'my-password');
91
+ ```
92
+
93
+ ## Supported Encryption
94
+
95
+ | Algorithm | PDF Version | Key Length | Status |
96
+ |-----------|-------------|-----------|--------|
97
+ | AES-256 (V=5, R=6) | 2.0 (ISO 32000-2) | 256-bit | Supported |
98
+ | AES-256 (V=5, R=5) | Adobe Extension Level 3 | 256-bit | Supported |
99
+ | RC4 (V=2, R=3) | 1.4+ (ISO 32000-1) | 128-bit | Supported |
100
+ | RC4 (V=1, R=2) | 1.1+ | 40-bit | Supported |
101
+ | AES-128 (V=4, R=4) | 1.6+ | 128-bit | Not yet supported |
102
+
103
+ ## Differences from upstream
104
+
105
+ | | `@pdfsmaller/pdf-decrypt` | `rn-pdf-decrypt` |
106
+ |---|---|---|
107
+ | Crypto backend | Web Crypto API (`crypto.subtle`) | `@noble/hashes` + `@noble/ciphers` |
108
+ | React Native | No (Hermes lacks Web Crypto) | Yes |
109
+ | AES-256 V=5/R=5 | No | Yes |
110
+ | Dependencies | Zero (peer: pdf-lib) | `@noble/hashes`, `@noble/ciphers` (peer: pdf-lib) |
111
+
112
+ ## License
113
+
114
+ MIT
115
+
116
+ Based on [@pdfsmaller/pdf-decrypt](https://www.npmjs.com/package/@pdfsmaller/pdf-decrypt) by [PDFSmaller.com](https://pdfsmaller.com).
@@ -0,0 +1,192 @@
1
+ /**
2
+ * AES-256 cryptographic utilities for PDF decryption (R=5/6)
3
+ * Uses @noble/hashes and @noble/ciphers — works in React Native (Hermes), browsers, Node.js, etc.
4
+ *
5
+ * @author imdewan (https://github.com/imdewan/rn-pdf-decrypt)
6
+ * @license MIT
7
+ *
8
+ * Includes encrypt-side functions (needed for Algorithm 2.B's AES-128-CBC encrypt step)
9
+ * plus decrypt functions for AES-256-CBC/ECB used in PDF object decryption.
10
+ *
11
+ * Implements Algorithm 2.B from ISO 32000-2:2020
12
+ * Verified against mozilla/pdf.js (the reference implementation)
13
+ */
14
+
15
+ const { sha256: _sha256, sha384: _sha384, sha512: _sha512 } = require('@noble/hashes/sha2.js');
16
+ const { cbc, ecb } = require('@noble/ciphers/aes.js');
17
+
18
+ /**
19
+ * Concatenate multiple Uint8Arrays
20
+ */
21
+ function concat(...arrays) {
22
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
23
+ const result = new Uint8Array(totalLength);
24
+ let offset = 0;
25
+ for (const arr of arrays) {
26
+ result.set(arr, offset);
27
+ offset += arr.length;
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // ========== SHA Hash Functions (@noble/hashes) ==========
33
+
34
+ async function sha256(data) {
35
+ return _sha256(data);
36
+ }
37
+
38
+ async function sha384(data) {
39
+ return _sha384(data);
40
+ }
41
+
42
+ async function sha512(data) {
43
+ return _sha512(data);
44
+ }
45
+
46
+ // ========== AES Encryption (@noble/ciphers) ==========
47
+ // These encrypt functions are needed because Algorithm 2.B uses aes128CbcEncrypt
48
+ // even during the decryption password validation flow.
49
+
50
+ /**
51
+ * AES-128-CBC encrypt (for Algorithm 2.B intermediate step)
52
+ * Strips PKCS#7 padding since input is always block-aligned
53
+ */
54
+ async function aes128CbcEncrypt(data, key, iv) {
55
+ const cipher = cbc(key, iv, { disablePadding: true });
56
+ return cipher.encrypt(data);
57
+ }
58
+
59
+ // ========== AES Decryption (@noble/ciphers) ==========
60
+
61
+ /**
62
+ * AES-256-CBC decrypt with PKCS#7 padding removal (for per-object decryption)
63
+ *
64
+ * @param {Uint8Array} data - Ciphertext (must be multiple of 16 bytes)
65
+ * @param {Uint8Array} key - 32-byte AES-256 key
66
+ * @param {Uint8Array} iv - 16-byte initialization vector
67
+ * @returns {Promise<Uint8Array>} - Decrypted plaintext
68
+ */
69
+ async function aes256CbcDecrypt(data, key, iv) {
70
+ const cipher = cbc(key, iv);
71
+ return cipher.decrypt(data);
72
+ }
73
+
74
+ /**
75
+ * AES-256-CBC decrypt without padding (for UE/OE — exactly 32 bytes, no PKCS#7)
76
+ *
77
+ * @param {Uint8Array} ciphertext - Ciphertext (32 bytes for UE/OE)
78
+ * @param {Uint8Array} key - 32-byte AES-256 key
79
+ * @param {Uint8Array} iv - 16-byte initialization vector
80
+ * @returns {Promise<Uint8Array>} - Decrypted plaintext (same length as ciphertext)
81
+ */
82
+ async function aes256CbcDecryptNoPad(ciphertext, key, iv) {
83
+ const cipher = cbc(key, iv, { disablePadding: true });
84
+ return cipher.decrypt(ciphertext);
85
+ }
86
+
87
+ /**
88
+ * AES-256-ECB decrypt a single 16-byte block (for Perms verification)
89
+ *
90
+ * @param {Uint8Array} block - 16-byte ciphertext block
91
+ * @param {Uint8Array} key - 32-byte AES-256 key
92
+ * @returns {Promise<Uint8Array>} - 16-byte decrypted block
93
+ */
94
+ async function aes256EcbDecryptBlock(block, key) {
95
+ const cipher = ecb(key, { disablePadding: true });
96
+ return cipher.decrypt(block);
97
+ }
98
+
99
+ /**
100
+ * Import an AES-256 key for reuse across multiple decrypt operations.
101
+ * With @noble/ciphers there's no CryptoKey concept — just return the raw key bytes.
102
+ *
103
+ * @param {Uint8Array} key - 32-byte AES-256 key
104
+ * @returns {Promise<Uint8Array>} - The same key bytes (for API compatibility)
105
+ */
106
+ async function importAES256DecryptKey(key) {
107
+ return key;
108
+ }
109
+
110
+ /**
111
+ * AES-256-CBC decrypt using a pre-imported key (for per-object bulk decryption)
112
+ * Handles PKCS#7 padding removal automatically.
113
+ *
114
+ * @param {Uint8Array} data - Ciphertext (must be multiple of 16 bytes)
115
+ * @param {Uint8Array} key - Raw 32-byte AES-256 key
116
+ * @param {Uint8Array} iv - 16-byte initialization vector
117
+ * @returns {Promise<Uint8Array>} - Decrypted plaintext
118
+ */
119
+ async function aes256CbcDecryptWithKey(data, key, iv) {
120
+ const cipher = cbc(key, iv);
121
+ return cipher.decrypt(data);
122
+ }
123
+
124
+ // ========== Algorithm 2.B (ISO 32000-2:2020) ==========
125
+
126
+ /**
127
+ * Algorithm 2.B — Computing a hash for R=6
128
+ *
129
+ * This is the hardened key derivation function used by PDF 2.0 (AES-256).
130
+ * Iterates SHA-256/384/512 + AES-128-CBC for at least 64 rounds.
131
+ *
132
+ * Note: Algorithm 2.B uses AES-128-CBC *encrypt* (not decrypt) even during
133
+ * the decryption/validation flow. This is by design per the PDF spec.
134
+ *
135
+ * Verified against mozilla/pdf.js (PDF20._hash)
136
+ *
137
+ * @param {Uint8Array} password - UTF-8 password bytes (max 127)
138
+ * @param {Uint8Array} salt - 8-byte salt
139
+ * @param {Uint8Array} userKey - 48-byte U value (for owner ops) or empty
140
+ * @returns {Promise<Uint8Array>} - 32-byte hash
141
+ */
142
+ async function computeHash2B(password, salt, userKey) {
143
+ // Step 1: Initial SHA-256 hash
144
+ const input = concat(password, salt, userKey);
145
+ let K = await sha256(input);
146
+
147
+ // Step 2: Iterative loop (minimum 64 rounds)
148
+ let i = 0;
149
+ let E;
150
+
151
+ while (true) {
152
+ // Step 2a: K1 = (password + K + userKey) repeated 64 times
153
+ const block = concat(password, K, userKey);
154
+ const K1 = new Uint8Array(block.length * 64);
155
+ for (let j = 0; j < 64; j++) {
156
+ K1.set(block, j * block.length);
157
+ }
158
+
159
+ // Step 2b: AES-128-CBC encrypt K1
160
+ // Key = K[0..15], IV = K[16..31]
161
+ const aesKey = K.slice(0, 16);
162
+ const aesIV = K.slice(16, 32);
163
+ E = await aes128CbcEncrypt(K1, aesKey, aesIV);
164
+
165
+ // Step 2c: Hash function selection
166
+ // Sum first 16 bytes of E mod 3 (equivalent to 128-bit big-endian mod 3)
167
+ let byteSum = 0;
168
+ for (let j = 0; j < 16; j++) {
169
+ byteSum += E[j];
170
+ }
171
+ const hashSelect = byteSum % 3;
172
+
173
+ // Step 2d: Hash E with selected function
174
+ if (hashSelect === 0) {
175
+ K = await sha256(E);
176
+ } else if (hashSelect === 1) {
177
+ K = await sha384(E);
178
+ } else {
179
+ K = await sha512(E);
180
+ }
181
+
182
+ // Step 2e: Termination (per pdf.js: while i < 64 || E[-1] > i - 32)
183
+ i++;
184
+ if (i >= 64 && E[E.length - 1] <= i - 32) {
185
+ break;
186
+ }
187
+ }
188
+
189
+ return K.slice(0, 32);
190
+ }
191
+
192
+ module.exports = { sha256, sha384, sha512, aes128CbcEncrypt, aes256CbcDecrypt, aes256CbcDecryptNoPad, aes256EcbDecryptBlock, importAES256DecryptKey, aes256CbcDecryptWithKey, computeHash2B, concat };
@@ -0,0 +1,190 @@
1
+ /**
2
+ * AES-256 cryptographic utilities for PDF decryption (R=5/6)
3
+ * Uses @noble/hashes and @noble/ciphers — works in React Native (Hermes), browsers, Node.js, etc.
4
+ *
5
+ * @author imdewan (https://github.com/imdewan/rn-pdf-decrypt)
6
+ * @license MIT
7
+ *
8
+ * Includes encrypt-side functions (needed for Algorithm 2.B's AES-128-CBC encrypt step)
9
+ * plus decrypt functions for AES-256-CBC/ECB used in PDF object decryption.
10
+ *
11
+ * Implements Algorithm 2.B from ISO 32000-2:2020
12
+ * Verified against mozilla/pdf.js (the reference implementation)
13
+ */
14
+
15
+ import { sha256 as _sha256, sha384 as _sha384, sha512 as _sha512 } from '@noble/hashes/sha2.js';
16
+ import { cbc, ecb } from '@noble/ciphers/aes.js';
17
+
18
+ /**
19
+ * Concatenate multiple Uint8Arrays
20
+ */
21
+ export function concat(...arrays) {
22
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
23
+ const result = new Uint8Array(totalLength);
24
+ let offset = 0;
25
+ for (const arr of arrays) {
26
+ result.set(arr, offset);
27
+ offset += arr.length;
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // ========== SHA Hash Functions (@noble/hashes) ==========
33
+
34
+ export async function sha256(data) {
35
+ return _sha256(data);
36
+ }
37
+
38
+ export async function sha384(data) {
39
+ return _sha384(data);
40
+ }
41
+
42
+ export async function sha512(data) {
43
+ return _sha512(data);
44
+ }
45
+
46
+ // ========== AES Encryption (@noble/ciphers) ==========
47
+ // These encrypt functions are needed because Algorithm 2.B uses aes128CbcEncrypt
48
+ // even during the decryption password validation flow.
49
+
50
+ /**
51
+ * AES-128-CBC encrypt (for Algorithm 2.B intermediate step)
52
+ * Strips PKCS#7 padding since input is always block-aligned
53
+ */
54
+ export async function aes128CbcEncrypt(data, key, iv) {
55
+ const cipher = cbc(key, iv, { disablePadding: true });
56
+ return cipher.encrypt(data);
57
+ }
58
+
59
+ // ========== AES Decryption (@noble/ciphers) ==========
60
+
61
+ /**
62
+ * AES-256-CBC decrypt with PKCS#7 padding removal (for per-object decryption)
63
+ *
64
+ * @param {Uint8Array} data - Ciphertext (must be multiple of 16 bytes)
65
+ * @param {Uint8Array} key - 32-byte AES-256 key
66
+ * @param {Uint8Array} iv - 16-byte initialization vector
67
+ * @returns {Promise<Uint8Array>} - Decrypted plaintext
68
+ */
69
+ export async function aes256CbcDecrypt(data, key, iv) {
70
+ const cipher = cbc(key, iv);
71
+ return cipher.decrypt(data);
72
+ }
73
+
74
+ /**
75
+ * AES-256-CBC decrypt without padding (for UE/OE — exactly 32 bytes, no PKCS#7)
76
+ *
77
+ * @param {Uint8Array} ciphertext - Ciphertext (32 bytes for UE/OE)
78
+ * @param {Uint8Array} key - 32-byte AES-256 key
79
+ * @param {Uint8Array} iv - 16-byte initialization vector
80
+ * @returns {Promise<Uint8Array>} - Decrypted plaintext (same length as ciphertext)
81
+ */
82
+ export async function aes256CbcDecryptNoPad(ciphertext, key, iv) {
83
+ const cipher = cbc(key, iv, { disablePadding: true });
84
+ return cipher.decrypt(ciphertext);
85
+ }
86
+
87
+ /**
88
+ * AES-256-ECB decrypt a single 16-byte block (for Perms verification)
89
+ *
90
+ * @param {Uint8Array} block - 16-byte ciphertext block
91
+ * @param {Uint8Array} key - 32-byte AES-256 key
92
+ * @returns {Promise<Uint8Array>} - 16-byte decrypted block
93
+ */
94
+ export async function aes256EcbDecryptBlock(block, key) {
95
+ const cipher = ecb(key, { disablePadding: true });
96
+ return cipher.decrypt(block);
97
+ }
98
+
99
+ /**
100
+ * Import an AES-256 key for reuse across multiple decrypt operations.
101
+ * With @noble/ciphers there's no CryptoKey concept — just return the raw key bytes.
102
+ *
103
+ * @param {Uint8Array} key - 32-byte AES-256 key
104
+ * @returns {Promise<Uint8Array>} - The same key bytes (for API compatibility)
105
+ */
106
+ export async function importAES256DecryptKey(key) {
107
+ return key;
108
+ }
109
+
110
+ /**
111
+ * AES-256-CBC decrypt using a pre-imported key (for per-object bulk decryption)
112
+ * Handles PKCS#7 padding removal automatically.
113
+ *
114
+ * @param {Uint8Array} data - Ciphertext (must be multiple of 16 bytes)
115
+ * @param {Uint8Array} key - Raw 32-byte AES-256 key
116
+ * @param {Uint8Array} iv - 16-byte initialization vector
117
+ * @returns {Promise<Uint8Array>} - Decrypted plaintext
118
+ */
119
+ export async function aes256CbcDecryptWithKey(data, key, iv) {
120
+ const cipher = cbc(key, iv);
121
+ return cipher.decrypt(data);
122
+ }
123
+
124
+ // ========== Algorithm 2.B (ISO 32000-2:2020) ==========
125
+
126
+ /**
127
+ * Algorithm 2.B — Computing a hash for R=6
128
+ *
129
+ * This is the hardened key derivation function used by PDF 2.0 (AES-256).
130
+ * Iterates SHA-256/384/512 + AES-128-CBC for at least 64 rounds.
131
+ *
132
+ * Note: Algorithm 2.B uses AES-128-CBC *encrypt* (not decrypt) even during
133
+ * the decryption/validation flow. This is by design per the PDF spec.
134
+ *
135
+ * Verified against mozilla/pdf.js (PDF20._hash)
136
+ *
137
+ * @param {Uint8Array} password - UTF-8 password bytes (max 127)
138
+ * @param {Uint8Array} salt - 8-byte salt
139
+ * @param {Uint8Array} userKey - 48-byte U value (for owner ops) or empty
140
+ * @returns {Promise<Uint8Array>} - 32-byte hash
141
+ */
142
+ export async function computeHash2B(password, salt, userKey) {
143
+ // Step 1: Initial SHA-256 hash
144
+ const input = concat(password, salt, userKey);
145
+ let K = await sha256(input);
146
+
147
+ // Step 2: Iterative loop (minimum 64 rounds)
148
+ let i = 0;
149
+ let E;
150
+
151
+ while (true) {
152
+ // Step 2a: K1 = (password + K + userKey) repeated 64 times
153
+ const block = concat(password, K, userKey);
154
+ const K1 = new Uint8Array(block.length * 64);
155
+ for (let j = 0; j < 64; j++) {
156
+ K1.set(block, j * block.length);
157
+ }
158
+
159
+ // Step 2b: AES-128-CBC encrypt K1
160
+ // Key = K[0..15], IV = K[16..31]
161
+ const aesKey = K.slice(0, 16);
162
+ const aesIV = K.slice(16, 32);
163
+ E = await aes128CbcEncrypt(K1, aesKey, aesIV);
164
+
165
+ // Step 2c: Hash function selection
166
+ // Sum first 16 bytes of E mod 3 (equivalent to 128-bit big-endian mod 3)
167
+ let byteSum = 0;
168
+ for (let j = 0; j < 16; j++) {
169
+ byteSum += E[j];
170
+ }
171
+ const hashSelect = byteSum % 3;
172
+
173
+ // Step 2d: Hash E with selected function
174
+ if (hashSelect === 0) {
175
+ K = await sha256(E);
176
+ } else if (hashSelect === 1) {
177
+ K = await sha384(E);
178
+ } else {
179
+ K = await sha512(E);
180
+ }
181
+
182
+ // Step 2e: Termination (per pdf.js: while i < 64 || E[-1] > i - 32)
183
+ i++;
184
+ if (i >= 64 && E[E.length - 1] <= i - 32) {
185
+ break;
186
+ }
187
+ }
188
+
189
+ return K.slice(0, 32);
190
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * rn-pdf-decrypt - RC4 cryptographic utilities
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
+ * This minimal cryptographic implementation was built to solve the "impossible"
10
+ * problem of real PDF encryption within Cloudflare Workers' 1MB limit.
11
+ * Total size: ~7KB for complete PDF encryption!
12
+ */
13
+
14
+ // Minimal cryptographic functions for PDF encryption
15
+ // Implements only what's needed for PDF Standard Security Handler
16
+
17
+ /**
18
+ * Minimal MD5 implementation
19
+ * Based on the MD5 algorithm - only what's needed for PDF encryption
20
+ * Part of rn-pdf-decrypt
21
+ */
22
+ function md5(data) {
23
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
24
+
25
+ // Initialize MD5 constants
26
+ const S = [
27
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
28
+ 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
29
+ 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
30
+ 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
31
+ ];
32
+
33
+ const K = new Uint32Array([
34
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
35
+ 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
36
+ 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
37
+ 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
38
+ 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
39
+ 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
40
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
41
+ 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
42
+ 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
43
+ 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
44
+ 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
45
+ 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
46
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
47
+ 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
48
+ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
49
+ 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
50
+ ]);
51
+
52
+ // Initialize hash values
53
+ let a0 = 0x67452301;
54
+ let b0 = 0xefcdab89;
55
+ let c0 = 0x98badcfe;
56
+ let d0 = 0x10325476;
57
+
58
+ // Pre-processing
59
+ const msgLen = bytes.length;
60
+ const msgBitLen = msgLen * 8;
61
+ const msgLenPadded = ((msgLen + 9 + 63) & ~63);
62
+ const msg = new Uint8Array(msgLenPadded);
63
+ msg.set(bytes);
64
+ msg[msgLen] = 0x80;
65
+
66
+ // Append length in bits
67
+ const dataView = new DataView(msg.buffer);
68
+ dataView.setUint32(msgLenPadded - 8, msgBitLen, true);
69
+ dataView.setUint32(msgLenPadded - 4, 0, true);
70
+
71
+ // Process message in 512-bit chunks
72
+ for (let offset = 0; offset < msgLenPadded; offset += 64) {
73
+ const chunk = new Uint32Array(msg.buffer, offset, 16);
74
+
75
+ let a = a0, b = b0, c = c0, d = d0;
76
+
77
+ for (let i = 0; i < 64; i++) {
78
+ let f, g;
79
+
80
+ if (i < 16) {
81
+ f = (b & c) | ((~b) & d);
82
+ g = i;
83
+ } else if (i < 32) {
84
+ f = (d & b) | ((~d) & c);
85
+ g = (5 * i + 1) % 16;
86
+ } else if (i < 48) {
87
+ f = b ^ c ^ d;
88
+ g = (3 * i + 5) % 16;
89
+ } else {
90
+ f = c ^ (b | (~d));
91
+ g = (7 * i) % 16;
92
+ }
93
+
94
+ f = (f + a + K[i] + chunk[g]) >>> 0;
95
+ a = d;
96
+ d = c;
97
+ c = b;
98
+ b = (b + ((f << S[i]) | (f >>> (32 - S[i])))) >>> 0;
99
+ }
100
+
101
+ a0 = (a0 + a) >>> 0;
102
+ b0 = (b0 + b) >>> 0;
103
+ c0 = (c0 + c) >>> 0;
104
+ d0 = (d0 + d) >>> 0;
105
+ }
106
+
107
+ // Produce the final hash value
108
+ const result = new Uint8Array(16);
109
+ const view = new DataView(result.buffer);
110
+ view.setUint32(0, a0, true);
111
+ view.setUint32(4, b0, true);
112
+ view.setUint32(8, c0, true);
113
+ view.setUint32(12, d0, true);
114
+
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * RC4 encryption/decryption
120
+ * RC4 is symmetric, so encryption and decryption are the same operation
121
+ * Part of rn-pdf-decrypt
122
+ */
123
+ class RC4 {
124
+ constructor(key) {
125
+ this.s = new Uint8Array(256);
126
+ this.i = 0;
127
+ this.j = 0;
128
+
129
+ // Key scheduling algorithm (KSA)
130
+ for (let i = 0; i < 256; i++) {
131
+ this.s[i] = i;
132
+ }
133
+
134
+ let j = 0;
135
+ for (let i = 0; i < 256; i++) {
136
+ j = (j + this.s[i] + key[i % key.length]) & 0xFF;
137
+ // Swap
138
+ [this.s[i], this.s[j]] = [this.s[j], this.s[i]];
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Encrypt/decrypt data
144
+ * @param {Uint8Array} data - Data to encrypt or decrypt
145
+ * @returns {Uint8Array} - Encrypted/decrypted data
146
+ */
147
+ process(data) {
148
+ const result = new Uint8Array(data.length);
149
+
150
+ for (let k = 0; k < data.length; k++) {
151
+ this.i = (this.i + 1) & 0xFF;
152
+ this.j = (this.j + this.s[this.i]) & 0xFF;
153
+
154
+ // Swap
155
+ [this.s[this.i], this.s[this.j]] = [this.s[this.j], this.s[this.i]];
156
+
157
+ const t = (this.s[this.i] + this.s[this.j]) & 0xFF;
158
+ result[k] = data[k] ^ this.s[t];
159
+ }
160
+
161
+ return result;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Convert hex string to Uint8Array
167
+ */
168
+ function hexToBytes(hex) {
169
+ const bytes = new Uint8Array(hex.length / 2);
170
+ for (let i = 0; i < bytes.length; i++) {
171
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
172
+ }
173
+ return bytes;
174
+ }
175
+
176
+ /**
177
+ * Convert Uint8Array to hex string
178
+ */
179
+ function bytesToHex(bytes) {
180
+ return Array.from(bytes)
181
+ .map(b => b.toString(16).padStart(2, '0'))
182
+ .join('');
183
+ }
184
+
185
+ module.exports = { md5, RC4, hexToBytes, bytesToHex };