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
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 };
|