pdf-lite 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.
Files changed (206) hide show
  1. package/.commitlintrc.cjs +25 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  4. package/.github/workflows/docs.yaml +93 -0
  5. package/.github/workflows/prepare-release.yaml +79 -0
  6. package/.github/workflows/release.yaml +80 -0
  7. package/.github/workflows/test.yaml +35 -0
  8. package/.husky/commit-msg +1 -0
  9. package/.husky/pre-commit +1 -0
  10. package/.prettierignore +4 -0
  11. package/.prettierrc +4 -0
  12. package/CONTRIBUTING.md +109 -0
  13. package/EXAMPLES.md +1515 -0
  14. package/LICENSE +21 -0
  15. package/README.md +285 -0
  16. package/examples/001-create-pdf.ts +112 -0
  17. package/examples/002-create-encrypted-pdf.ts +121 -0
  18. package/examples/003-sign-pdf.ts +347 -0
  19. package/examples/004-incremental-update.ts +206 -0
  20. package/examples/005-modify-acroform.ts +374 -0
  21. package/examples/006-tokeniser-example.ts +131 -0
  22. package/examples/007-decoder-example.ts +197 -0
  23. package/package.json +72 -0
  24. package/packages/pdf-lite/README.md +3 -0
  25. package/packages/pdf-lite/package.json +68 -0
  26. package/packages/pdf-lite/scripts/create-encryption-tests.sh +41 -0
  27. package/packages/pdf-lite/scripts/gen-signing-keys.sh +290 -0
  28. package/packages/pdf-lite/scripts/generate-all-signing-keys.sh +70 -0
  29. package/packages/pdf-lite/src/core/decoder.ts +454 -0
  30. package/packages/pdf-lite/src/core/generators.ts +128 -0
  31. package/packages/pdf-lite/src/core/incremental-parser.ts +221 -0
  32. package/packages/pdf-lite/src/core/index.ts +2 -0
  33. package/packages/pdf-lite/src/core/objects/pdf-array.ts +54 -0
  34. package/packages/pdf-lite/src/core/objects/pdf-boolean.ts +19 -0
  35. package/packages/pdf-lite/src/core/objects/pdf-comment.ts +50 -0
  36. package/packages/pdf-lite/src/core/objects/pdf-date.ts +74 -0
  37. package/packages/pdf-lite/src/core/objects/pdf-dictionary.ts +171 -0
  38. package/packages/pdf-lite/src/core/objects/pdf-hexadecimal.ts +54 -0
  39. package/packages/pdf-lite/src/core/objects/pdf-indirect-object.ts +137 -0
  40. package/packages/pdf-lite/src/core/objects/pdf-name.ts +19 -0
  41. package/packages/pdf-lite/src/core/objects/pdf-null.ts +15 -0
  42. package/packages/pdf-lite/src/core/objects/pdf-number.ts +98 -0
  43. package/packages/pdf-lite/src/core/objects/pdf-object-reference.ts +30 -0
  44. package/packages/pdf-lite/src/core/objects/pdf-object.ts +107 -0
  45. package/packages/pdf-lite/src/core/objects/pdf-start-xref.ts +39 -0
  46. package/packages/pdf-lite/src/core/objects/pdf-stream.ts +687 -0
  47. package/packages/pdf-lite/src/core/objects/pdf-string.ts +38 -0
  48. package/packages/pdf-lite/src/core/objects/pdf-trailer.ts +57 -0
  49. package/packages/pdf-lite/src/core/objects/pdf-xref-table.ts +264 -0
  50. package/packages/pdf-lite/src/core/parser.ts +22 -0
  51. package/packages/pdf-lite/src/core/ref.ts +102 -0
  52. package/packages/pdf-lite/src/core/serializer.ts +68 -0
  53. package/packages/pdf-lite/src/core/streams/object-stream.ts +20 -0
  54. package/packages/pdf-lite/src/core/tokeniser.ts +687 -0
  55. package/packages/pdf-lite/src/core/tokens/boolean-token.ts +20 -0
  56. package/packages/pdf-lite/src/core/tokens/byte-offset-token.ts +20 -0
  57. package/packages/pdf-lite/src/core/tokens/comment-token.ts +32 -0
  58. package/packages/pdf-lite/src/core/tokens/end-array-token.ts +10 -0
  59. package/packages/pdf-lite/src/core/tokens/end-dictionary-token.ts +10 -0
  60. package/packages/pdf-lite/src/core/tokens/end-object-token.ts +10 -0
  61. package/packages/pdf-lite/src/core/tokens/end-stream-token.ts +11 -0
  62. package/packages/pdf-lite/src/core/tokens/hexadecimal-token.ts +22 -0
  63. package/packages/pdf-lite/src/core/tokens/name-token.ts +19 -0
  64. package/packages/pdf-lite/src/core/tokens/null-token.ts +9 -0
  65. package/packages/pdf-lite/src/core/tokens/number-token.ts +164 -0
  66. package/packages/pdf-lite/src/core/tokens/object-reference-token.ts +24 -0
  67. package/packages/pdf-lite/src/core/tokens/start-array-token.ts +10 -0
  68. package/packages/pdf-lite/src/core/tokens/start-dictionary-token.ts +10 -0
  69. package/packages/pdf-lite/src/core/tokens/start-object-token.ts +28 -0
  70. package/packages/pdf-lite/src/core/tokens/start-stream-token.ts +52 -0
  71. package/packages/pdf-lite/src/core/tokens/start-xref-token.ts +10 -0
  72. package/packages/pdf-lite/src/core/tokens/stream-chunk-token.ts +8 -0
  73. package/packages/pdf-lite/src/core/tokens/string-token.ts +17 -0
  74. package/packages/pdf-lite/src/core/tokens/token.ts +43 -0
  75. package/packages/pdf-lite/src/core/tokens/trailer-token.ts +12 -0
  76. package/packages/pdf-lite/src/core/tokens/whitespace-token.ts +43 -0
  77. package/packages/pdf-lite/src/core/tokens/xref-table-entry-token.ts +65 -0
  78. package/packages/pdf-lite/src/core/tokens/xref-table-section-start-token.ts +31 -0
  79. package/packages/pdf-lite/src/core/tokens/xref-table-start-token.ts +13 -0
  80. package/packages/pdf-lite/src/crypto/ciphers/aes128.ts +63 -0
  81. package/packages/pdf-lite/src/crypto/ciphers/aes256.ts +50 -0
  82. package/packages/pdf-lite/src/crypto/ciphers/rc4.ts +82 -0
  83. package/packages/pdf-lite/src/crypto/constants.ts +10 -0
  84. package/packages/pdf-lite/src/crypto/key-derivation/key-derivation-aes256.ts +213 -0
  85. package/packages/pdf-lite/src/crypto/key-derivation/key-derivation.ts +122 -0
  86. package/packages/pdf-lite/src/crypto/key-gen/key-gen-aes256.ts +79 -0
  87. package/packages/pdf-lite/src/crypto/key-gen/key-gen-rc4-128.ts +190 -0
  88. package/packages/pdf-lite/src/crypto/key-gen/key-gen-rc4-40.ts +129 -0
  89. package/packages/pdf-lite/src/crypto/types.ts +6 -0
  90. package/packages/pdf-lite/src/crypto/utils.ts +81 -0
  91. package/packages/pdf-lite/src/filters/ascii85.ts +128 -0
  92. package/packages/pdf-lite/src/filters/asciihex.ts +55 -0
  93. package/packages/pdf-lite/src/filters/flate.ts +39 -0
  94. package/packages/pdf-lite/src/filters/lzw.ts +144 -0
  95. package/packages/pdf-lite/src/filters/pass-through.ts +37 -0
  96. package/packages/pdf-lite/src/filters/runlength.ts +92 -0
  97. package/packages/pdf-lite/src/filters/types.ts +21 -0
  98. package/packages/pdf-lite/src/index.ts +4 -0
  99. package/packages/pdf-lite/src/pdf/errors.ts +5 -0
  100. package/packages/pdf-lite/src/pdf/index.ts +4 -0
  101. package/packages/pdf-lite/src/pdf/pdf-document.ts +924 -0
  102. package/packages/pdf-lite/src/pdf/pdf-reader.ts +57 -0
  103. package/packages/pdf-lite/src/pdf/pdf-revision.ts +234 -0
  104. package/packages/pdf-lite/src/pdf/pdf-xref-lookup.ts +527 -0
  105. package/packages/pdf-lite/src/security/crypt-filters/aesv2.ts +58 -0
  106. package/packages/pdf-lite/src/security/crypt-filters/aesv3.ts +56 -0
  107. package/packages/pdf-lite/src/security/crypt-filters/base.ts +140 -0
  108. package/packages/pdf-lite/src/security/crypt-filters/identity.ts +40 -0
  109. package/packages/pdf-lite/src/security/crypt-filters/v2.ts +59 -0
  110. package/packages/pdf-lite/src/security/handlers/base.ts +625 -0
  111. package/packages/pdf-lite/src/security/handlers/pubSec.ts +413 -0
  112. package/packages/pdf-lite/src/security/handlers/utils.ts +304 -0
  113. package/packages/pdf-lite/src/security/handlers/v1.ts +225 -0
  114. package/packages/pdf-lite/src/security/handlers/v2.ts +128 -0
  115. package/packages/pdf-lite/src/security/handlers/v4.ts +379 -0
  116. package/packages/pdf-lite/src/security/handlers/v5.ts +298 -0
  117. package/packages/pdf-lite/src/security/types.ts +158 -0
  118. package/packages/pdf-lite/src/signing/document-security-store.ts +224 -0
  119. package/packages/pdf-lite/src/signing/index.ts +3 -0
  120. package/packages/pdf-lite/src/signing/signatures/adbe-pkcs7-detached.ts +154 -0
  121. package/packages/pdf-lite/src/signing/signatures/adbe-pkcs7-sha1.ts +161 -0
  122. package/packages/pdf-lite/src/signing/signatures/adbe-x509-rsa-sha1.ts +106 -0
  123. package/packages/pdf-lite/src/signing/signatures/base.ts +229 -0
  124. package/packages/pdf-lite/src/signing/signatures/etsi-cades-detached.ts +229 -0
  125. package/packages/pdf-lite/src/signing/signatures/etsi-rfc3161.ts +92 -0
  126. package/packages/pdf-lite/src/signing/signatures/index.ts +6 -0
  127. package/packages/pdf-lite/src/signing/signer.ts +120 -0
  128. package/packages/pdf-lite/src/signing/types.ts +86 -0
  129. package/packages/pdf-lite/src/signing/utils.ts +71 -0
  130. package/packages/pdf-lite/src/types.ts +44 -0
  131. package/packages/pdf-lite/src/utils/IterableReadableStream.ts +30 -0
  132. package/packages/pdf-lite/src/utils/algos.ts +446 -0
  133. package/packages/pdf-lite/src/utils/assert.ts +42 -0
  134. package/packages/pdf-lite/src/utils/bytesToHex.ts +18 -0
  135. package/packages/pdf-lite/src/utils/bytesToHexBytes.ts +27 -0
  136. package/packages/pdf-lite/src/utils/bytesToString.ts +17 -0
  137. package/packages/pdf-lite/src/utils/concatUint8Arrays.ts +26 -0
  138. package/packages/pdf-lite/src/utils/escapeString.ts +49 -0
  139. package/packages/pdf-lite/src/utils/hexBytesToBytes.ts +22 -0
  140. package/packages/pdf-lite/src/utils/hexBytesToString.ts +21 -0
  141. package/packages/pdf-lite/src/utils/hexToBytes.ts +18 -0
  142. package/packages/pdf-lite/src/utils/padBytes.ts +25 -0
  143. package/packages/pdf-lite/src/utils/predictors.ts +332 -0
  144. package/packages/pdf-lite/src/utils/replaceInBuffer.ts +56 -0
  145. package/packages/pdf-lite/src/utils/stringToBytes.ts +22 -0
  146. package/packages/pdf-lite/src/utils/stringToHexBytes.ts +23 -0
  147. package/packages/pdf-lite/src/utils/unescapeString.ts +123 -0
  148. package/packages/pdf-lite/test/acceptance/__snapshots__/versions.node.test.ts.snap +60766 -0
  149. package/packages/pdf-lite/test/acceptance/fixtures/1.3/basic.pdf +0 -0
  150. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-aes-128.pdf +0 -0
  151. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-aes-256.pdf +0 -0
  152. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-rc4-128.pdf +0 -0
  153. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-rc4-40.pdf +0 -0
  154. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic.pdf +0 -0
  155. package/packages/pdf-lite/test/acceptance/fixtures/1.5/basic.pdf +0 -0
  156. package/packages/pdf-lite/test/acceptance/fixtures/1.6/basic.pdf +0 -0
  157. package/packages/pdf-lite/test/acceptance/fixtures/1.7/basic.pdf +0 -0
  158. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-aes-128.pdf +43 -0
  159. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-aes-256.pdf +43 -0
  160. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-rc4-128.pdf +43 -0
  161. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-rc4-40.pdf +44 -0
  162. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic.pdf +79 -0
  163. package/packages/pdf-lite/test/acceptance/versions.node.test.ts +41 -0
  164. package/packages/pdf-lite/test/unit/__snapshots__/decoder.node.test.ts.snap +86947 -0
  165. package/packages/pdf-lite/test/unit/__snapshots__/tokeniser.node.test.ts.snap +131829 -0
  166. package/packages/pdf-lite/test/unit/ciphers.test.ts +61 -0
  167. package/packages/pdf-lite/test/unit/decoder.node.test.ts +21 -0
  168. package/packages/pdf-lite/test/unit/decoder.test.ts +567 -0
  169. package/packages/pdf-lite/test/unit/filters.test.ts +67 -0
  170. package/packages/pdf-lite/test/unit/fixtures/basic.pdf +0 -0
  171. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-aes-128.pdf +0 -0
  172. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-aes-256.pdf +0 -0
  173. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-rc4-128.pdf +0 -0
  174. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-rc4-40.pdf +43 -0
  175. package/packages/pdf-lite/test/unit/fixtures/protectedAdobeLivecycle.pdf +0 -0
  176. package/packages/pdf-lite/test/unit/fixtures/rsa-2048/index.ts +187 -0
  177. package/packages/pdf-lite/test/unit/fixtures/template.pdf +0 -0
  178. package/packages/pdf-lite/test/unit/incremental-update.test.ts +0 -0
  179. package/packages/pdf-lite/test/unit/objects.test.ts +0 -0
  180. package/packages/pdf-lite/test/unit/pdf-document-signing.test.ts +0 -0
  181. package/packages/pdf-lite/test/unit/pdf-revision.test.ts +195 -0
  182. package/packages/pdf-lite/test/unit/pdf.browser.test.ts +0 -0
  183. package/packages/pdf-lite/test/unit/predictors.test.ts +226 -0
  184. package/packages/pdf-lite/test/unit/ref.test.ts +158 -0
  185. package/packages/pdf-lite/test/unit/security-handlers.test.ts +645 -0
  186. package/packages/pdf-lite/test/unit/serializer.test.ts +81 -0
  187. package/packages/pdf-lite/test/unit/signature-objects.test.ts +814 -0
  188. package/packages/pdf-lite/test/unit/string-escaping.test.ts +84 -0
  189. package/packages/pdf-lite/test/unit/tokeniser.node.test.ts +38 -0
  190. package/packages/pdf-lite/test/unit/tokeniser.test.ts +1213 -0
  191. package/packages/pdf-lite/test/unit/utils.test.ts +248 -0
  192. package/packages/pdf-lite/test/unit/xref-lookup.test.ts +72 -0
  193. package/packages/pdf-lite/tsconfig.json +4 -0
  194. package/packages/pdf-lite/tsconfig.prod.json +8 -0
  195. package/packages/pdf-lite/typedoc.json +14 -0
  196. package/packages/pdf-lite/vitest.config.ts +43 -0
  197. package/pnpm-workspace.yaml +2 -0
  198. package/renovate.json +34 -0
  199. package/scripts/build-examples.ts +30 -0
  200. package/scripts/bump-version.sh +56 -0
  201. package/scripts/gen-html-docs.sh +21 -0
  202. package/scripts/gen-md-docs.sh +15 -0
  203. package/scripts/prepare-release.sh +33 -0
  204. package/tsconfig.json +22 -0
  205. package/tsconfig.prod.json +12 -0
  206. package/typedoc.json +34 -0
@@ -0,0 +1,31 @@
1
+ import { ByteArray } from '../../types'
2
+ import { stringToBytes } from '../../utils/stringToBytes'
3
+ import { PdfNumberToken } from './number-token'
4
+ import { PdfToken } from './token'
5
+
6
+ export class PdfXRefTableSectionStartToken extends PdfToken {
7
+ start: PdfNumberToken
8
+ count: PdfNumberToken
9
+
10
+ constructor(
11
+ start: number | PdfNumberToken,
12
+ count: number | PdfNumberToken,
13
+ ) {
14
+ super(PdfXRefTableSectionStartToken.toBytes(start, count))
15
+ this.start =
16
+ start instanceof PdfNumberToken
17
+ ? start
18
+ : new PdfNumberToken({ value: start })
19
+ this.count =
20
+ count instanceof PdfNumberToken
21
+ ? count
22
+ : new PdfNumberToken({ value: count })
23
+ }
24
+
25
+ private static toBytes(
26
+ start: PdfNumberToken | number,
27
+ count: PdfNumberToken | number,
28
+ ): ByteArray {
29
+ return stringToBytes(`${start.toString()} ${count.toString()}`)
30
+ }
31
+ }
@@ -0,0 +1,13 @@
1
+ import { stringToBytes } from '../../utils/stringToBytes'
2
+ import { PdfToken } from './token'
3
+
4
+ const XREF = stringToBytes('xref')
5
+
6
+ export class PdfXRefTableStartToken extends PdfToken {
7
+ byteOffset?: number
8
+
9
+ constructor(byteOffset?: number) {
10
+ super(XREF)
11
+ this.byteOffset = byteOffset
12
+ }
13
+ }
@@ -0,0 +1,63 @@
1
+ import { ByteArray } from '../../types.js'
2
+ import { aes128cbcDecrypt, aes128cbcEncrypt } from '../../utils/algos.js'
3
+ import { Cipher } from '../types.js'
4
+
5
+ /**
6
+ * Creates an AES-128-CBC cipher for PDF encryption.
7
+ * The cipher prepends a zero IV to encrypted data during encryption
8
+ * and extracts the IV from the first 16 bytes during decryption.
9
+ *
10
+ * @param key - The 16-byte encryption key.
11
+ * @returns A Cipher object with encrypt and decrypt methods.
12
+ * @throws Error if the key is not exactly 16 bytes.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const cipher = aes128(key)
17
+ * const encrypted = await cipher.encrypt(plaintext)
18
+ * const decrypted = await cipher.decrypt(encrypted)
19
+ * ```
20
+ */
21
+ export function aes128(key: ByteArray): Cipher {
22
+ if (key.length !== 16)
23
+ throw new Error(
24
+ `AES-128 key must be exactly 16 bytes, got ${key.length}`,
25
+ )
26
+
27
+ const iv = new Uint8Array(16) // Zero IV, as per PDF spec
28
+
29
+ return {
30
+ /**
31
+ * Encrypts data using AES-128-CBC.
32
+ * Prepends the IV to the ciphertext.
33
+ *
34
+ * @param data - The data to encrypt.
35
+ * @returns A promise that resolves to IV followed by ciphertext.
36
+ * @throws Error if the IV is not exactly 16 bytes.
37
+ */
38
+ encrypt: async (data: ByteArray): Promise<ByteArray> => {
39
+ if (iv.length !== 16) throw new Error('IV must be exactly 16 bytes')
40
+
41
+ const encrypted = await aes128cbcEncrypt(key, data)
42
+ const result = new Uint8Array(iv.length + encrypted.length)
43
+
44
+ result.set(iv, 0)
45
+ result.set(encrypted, iv.length)
46
+
47
+ return result
48
+ },
49
+ /**
50
+ * Decrypts data using AES-128-CBC.
51
+ * Extracts the IV from the first 16 bytes of the input.
52
+ *
53
+ * @param data - The data to decrypt (IV + ciphertext).
54
+ * @returns A promise that resolves to the decrypted plaintext.
55
+ */
56
+ decrypt: async (data: ByteArray): Promise<ByteArray> => {
57
+ const iv = data.slice(0, 16)
58
+ const ciphertext = data.slice(16)
59
+
60
+ return await aes128cbcDecrypt(key, ciphertext, iv)
61
+ },
62
+ }
63
+ }
@@ -0,0 +1,50 @@
1
+ import { ByteArray } from '../../types.js'
2
+ import { aes256cbcDecrypt, aes256cbcEncrypt } from '../../utils/algos.js'
3
+ import { Cipher } from '../types.js'
4
+
5
+ /**
6
+ * Creates an AES-256-CBC cipher for PDF encryption.
7
+ * The cipher prepends a zero IV to encrypted data during encryption
8
+ * and extracts the IV from the first 16 bytes during decryption.
9
+ *
10
+ * @param key - The 32-byte encryption key.
11
+ * @returns A Cipher object with encrypt and decrypt methods.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const cipher = aes256(key)
16
+ * const encrypted = await cipher.encrypt(plaintext)
17
+ * const decrypted = await cipher.decrypt(encrypted)
18
+ * ```
19
+ */
20
+ export function aes256(key: ByteArray): Cipher {
21
+ const iv = new Uint8Array(16) // Zero IV, as per PDF spec
22
+ return {
23
+ /**
24
+ * Encrypts data using AES-256-CBC.
25
+ * Prepends the IV to the ciphertext.
26
+ *
27
+ * @param data - The data to encrypt.
28
+ * @returns A promise that resolves to IV followed by ciphertext.
29
+ */
30
+ encrypt: async (data: ByteArray): Promise<ByteArray> => {
31
+ const encrypted = await aes256cbcEncrypt(key, data, iv)
32
+ const result = new Uint8Array(iv.length + encrypted.length)
33
+ result.set(iv, 0)
34
+ result.set(encrypted, iv.length)
35
+ return result
36
+ },
37
+ /**
38
+ * Decrypts data using AES-256-CBC.
39
+ * Extracts the IV from the first 16 bytes of the input.
40
+ *
41
+ * @param data - The data to decrypt (IV + ciphertext).
42
+ * @returns A promise that resolves to the decrypted plaintext.
43
+ */
44
+ decrypt: async (data: ByteArray): Promise<ByteArray> => {
45
+ const iv = data.slice(0, 16)
46
+ const ciphertext = data.slice(16)
47
+ return await aes256cbcDecrypt(key, ciphertext, iv)
48
+ },
49
+ }
50
+ }
@@ -0,0 +1,82 @@
1
+ import { ByteArray } from '../../types.js'
2
+ import { Cipher } from '../types.js'
3
+
4
+ /**
5
+ * Creates an RC4 cipher for PDF encryption.
6
+ * RC4 is a symmetric stream cipher where encryption and decryption
7
+ * use the same operation (XOR with the key stream).
8
+ *
9
+ * @param key - The encryption key (variable length).
10
+ * @returns A Cipher object with encrypt and decrypt methods.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const cipher = rc4(key)
15
+ * const encrypted = await cipher.encrypt(plaintext)
16
+ * const decrypted = await cipher.decrypt(encrypted)
17
+ * ```
18
+ */
19
+ export function rc4(key: ByteArray): Cipher {
20
+ /**
21
+ * RC4 Key Scheduling Algorithm (KSA).
22
+ * Initializes the permutation in the S array based on the key.
23
+ *
24
+ * @param key - The encryption key.
25
+ * @returns The initialized S array (256 bytes).
26
+ */
27
+ function ksa(key: ByteArray) {
28
+ const S = new Uint8Array(256)
29
+ for (let i = 0; i < 256; i++) S[i] = i
30
+ let j = 0
31
+ for (let i = 0; i < 256; i++) {
32
+ j = (j + S[i] + key[i % key.length]) & 0xff
33
+ ;[S[i], S[j]] = [S[j], S[i]]
34
+ }
35
+ return S
36
+ }
37
+
38
+ /**
39
+ * RC4 Pseudo-Random Generation Algorithm (PRGA).
40
+ * Generates the key stream and XORs it with the data.
41
+ *
42
+ * @param data - The data to encrypt or decrypt.
43
+ * @param S - The initialized S array from KSA.
44
+ * @returns The encrypted or decrypted data.
45
+ */
46
+ function rc4(data: ByteArray, S: ByteArray): ByteArray {
47
+ const out = new Uint8Array(data.length)
48
+ let i = 0,
49
+ j = 0
50
+ const s = S.slice() // Copy S for each operation
51
+ for (let k = 0; k < data.length; k++) {
52
+ i = (i + 1) & 0xff
53
+ j = (j + s[i]) & 0xff
54
+ ;[s[i], s[j]] = [s[j], s[i]]
55
+ const rnd = s[(s[i] + s[j]) & 0xff]
56
+ out[k] = data[k] ^ rnd
57
+ }
58
+ return out
59
+ }
60
+
61
+ const S = ksa(key)
62
+ return {
63
+ /**
64
+ * Encrypts data using RC4.
65
+ *
66
+ * @param data - The data to encrypt.
67
+ * @returns A promise that resolves to the encrypted data.
68
+ */
69
+ encrypt: async (data: ByteArray): Promise<ByteArray> => {
70
+ return rc4(data, S)
71
+ },
72
+ /**
73
+ * Decrypts data using RC4.
74
+ *
75
+ * @param data - The data to decrypt.
76
+ * @returns A promise that resolves to the decrypted data.
77
+ */
78
+ decrypt: async (data: ByteArray): Promise<ByteArray> => {
79
+ return rc4(data, S)
80
+ },
81
+ }
82
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Default password padding used in PDF encryption.
3
+ * This 32-byte constant is defined in the PDF specification and is used
4
+ * to pad passwords shorter than 32 bytes during encryption key derivation.
5
+ */
6
+ export const DEFAULT_PADDING = new Uint8Array([
7
+ 0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56,
8
+ 0xff, 0xfa, 0x01, 0x08, 0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80,
9
+ 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a,
10
+ ])
@@ -0,0 +1,213 @@
1
+ import { ByteArray } from '../../types'
2
+ import {
3
+ aes128CbcNoPaddingEncrypt,
4
+ aes256CbcNoPaddingDecrypt,
5
+ sha256,
6
+ sha384,
7
+ sha512,
8
+ } from '../../utils/algos'
9
+ import { assert } from '../../utils/assert'
10
+
11
+ /**
12
+ * Converts the first 16 bytes of input to a big-endian bigint.
13
+ *
14
+ * @param input - The byte array to convert.
15
+ * @returns A 128-bit bigint value.
16
+ */
17
+ function getUint16ByteBigEndian(input: ByteArray): bigint {
18
+ let value = 0n
19
+ for (let i = 0; i < 16; i++) {
20
+ value = (value << 8n) | BigInt(input[i])
21
+ }
22
+ return value
23
+ }
24
+
25
+ /**
26
+ * Computes the Algorithm 2.B hash for PDF 2.0 AES-256 encryption.
27
+ * This iterative hash algorithm uses SHA-256, SHA-384, or SHA-512 based on
28
+ * intermediate results, running for at least 64 rounds.
29
+ *
30
+ * @param password - The user or owner password.
31
+ * @param salt - The 8-byte validation or key salt.
32
+ * @param userKey - The user key (required for owner password validation). Defaults to empty.
33
+ * @returns A promise that resolves to a 32-byte hash.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const hash = await computeAlgorithm2bHash(password, salt)
38
+ * ```
39
+ */
40
+ export async function computeAlgorithm2bHash(
41
+ password: ByteArray,
42
+ salt: ByteArray,
43
+ userKey: ByteArray = new Uint8Array(),
44
+ ): Promise<ByteArray> {
45
+ // Step 1: Initial hash
46
+ let K = new Uint8Array(password.length + salt.length + userKey.length)
47
+ K.set(password)
48
+ K.set(salt, password.length)
49
+ K.set(userKey, password.length + salt.length)
50
+
51
+ K = await sha256(K)
52
+
53
+ let roundNumber = 0
54
+ let E: ByteArray = new Uint8Array()
55
+ while (roundNumber < 64 || E.at(-1)! > roundNumber - 32) {
56
+ // Step 2: K1 = 64 × (password || K || [userKey])
57
+ const base = new Uint8Array(password.length + K.length + userKey.length)
58
+ base.set(password)
59
+ base.set(K, password.length)
60
+ base.set(userKey, password.length + K.length)
61
+
62
+ const K1 = new Uint8Array(base.length * 64)
63
+ for (let i = 0; i < 64; i++) {
64
+ K1.set(base, i * base.length)
65
+ }
66
+
67
+ // Step 3: Encrypt K1 with AES-128-CBC using key/iv from K
68
+ const key = K.subarray(0, 16)
69
+ const iv = K.subarray(16, 32)
70
+ E = await aes128CbcNoPaddingEncrypt(key, K1, iv)
71
+
72
+ // Step 4: Choose hash
73
+ const value = getUint16ByteBigEndian(E)
74
+ const mod = Number(value % 3n)
75
+
76
+ if (mod === 0) {
77
+ K = await sha256(E)
78
+ } else if (mod === 1) {
79
+ K = await sha384(E)
80
+ } else if (mod === 2) {
81
+ K = await sha512(E)
82
+ } else {
83
+ throw new Error('Invalid mod')
84
+ }
85
+
86
+ roundNumber++
87
+ }
88
+
89
+ return K.subarray(0, 32)
90
+ }
91
+
92
+ /**
93
+ * Validates a password against a stored hash using the Algorithm 2.B hash.
94
+ *
95
+ * @param password - The password to validate.
96
+ * @param key - The stored key containing the hash (first 32 bytes) and validation salt (bytes 32-40).
97
+ * @param extra - Extra data for owner password validation (user key).
98
+ * @returns A promise that resolves to the computed hash if validation succeeds.
99
+ * @throws Error if the password is invalid or salt/hash lengths are incorrect.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * try {
104
+ * await validatePasswordHash(password, storedKey)
105
+ * console.log('Password is valid')
106
+ * } catch (e) {
107
+ * console.log('Invalid password')
108
+ * }
109
+ * ```
110
+ */
111
+ export async function validatePasswordHash(
112
+ password: ByteArray,
113
+ key: ByteArray,
114
+ extra?: ByteArray,
115
+ ): Promise<ByteArray> {
116
+ const validationSalt = key.slice(32, 40)
117
+ if (validationSalt.length !== 8) {
118
+ throw new Error('Invalid salt length')
119
+ }
120
+
121
+ const hash = await computeAlgorithm2bHash(password, validationSalt, extra)
122
+
123
+ if (hash.length !== 32) {
124
+ throw new Error('Invalid hash length')
125
+ }
126
+
127
+ for (let i = 0; i < 32; i++) {
128
+ if (hash[i] !== key[i]) {
129
+ throw new Error('Key mismatch')
130
+ }
131
+ }
132
+
133
+ return hash
134
+ }
135
+
136
+ /**
137
+ * Retrieves the file encryption key using user or owner password.
138
+ * Tries owner password first, then falls back to user password.
139
+ *
140
+ * @param userPassword - The user password to try.
141
+ * @param ownerPassword - The owner password to try.
142
+ * @param u - The 48-byte /U value from the encryption dictionary.
143
+ * @param ue - The 32-byte /UE value (encrypted user key).
144
+ * @param o - The 48-byte /O value from the encryption dictionary.
145
+ * @param oe - The 32-byte /OE value (encrypted owner key).
146
+ * @returns A promise that resolves to the 32-byte file encryption key.
147
+ * @throws Error if both passwords are invalid.
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const fileKey = await getFileKey(userPw, ownerPw, U, UE, O, OE)
152
+ * ```
153
+ */
154
+ export async function getFileKey(
155
+ userPassword: ByteArray,
156
+ ownerPassword: ByteArray,
157
+ u: ByteArray,
158
+ ue: ByteArray,
159
+ o: ByteArray,
160
+ oe: ByteArray,
161
+ ): Promise<ByteArray> {
162
+ if (userPassword.length > 128) {
163
+ userPassword = userPassword.subarray(0, 128)
164
+ }
165
+
166
+ if (ownerPassword.length > 128) {
167
+ ownerPassword = ownerPassword.subarray(0, 128)
168
+ }
169
+
170
+ assert(oe.length === 32, 'Invalid OE length')
171
+ assert(ue.length === 32, 'Invalid UE length')
172
+ assert(u.length === 48, 'Invalid U length')
173
+ assert(o.length === 48, 'Invalid O length')
174
+
175
+ try {
176
+ // First try owner password
177
+ await validatePasswordHash(ownerPassword, o, u)
178
+
179
+ const hash = await computeAlgorithm2bHash(
180
+ ownerPassword,
181
+ o.subarray(40, 48),
182
+ u,
183
+ )
184
+
185
+ const key = await aes256CbcNoPaddingDecrypt(
186
+ hash,
187
+ oe,
188
+ new Uint8Array(16),
189
+ )
190
+
191
+ return key
192
+ } catch (e) {
193
+ try {
194
+ // Then try user password
195
+
196
+ await validatePasswordHash(userPassword, u)
197
+
198
+ const hash = await computeAlgorithm2bHash(
199
+ userPassword,
200
+ u.subarray(40, 48),
201
+ )
202
+ const key = await aes256CbcNoPaddingDecrypt(
203
+ hash,
204
+ ue,
205
+ new Uint8Array(16),
206
+ )
207
+
208
+ return key
209
+ } catch (e) {
210
+ throw new Error('Invalid password')
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,122 @@
1
+ import { DEFAULT_PADDING } from '../constants.js'
2
+ import { md5 } from '../../utils/algos.js'
3
+ import { int32ToLittleEndianBytes } from '../utils.js'
4
+ import { concatUint8Arrays } from '../../utils/concatUint8Arrays.js'
5
+ import { ByteArray } from '../../types.js'
6
+
7
+ /**
8
+ * Pads a password to exactly 32 bytes using the PDF standard padding.
9
+ * If the password is shorter than 32 bytes, it is padded with bytes from DEFAULT_PADDING.
10
+ * If the password is 32 bytes or longer, only the first 32 bytes are used.
11
+ *
12
+ * @param password - The password to pad.
13
+ * @returns A 32-byte padded password.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const padded = padPassword(new Uint8Array([1, 2, 3])) // Returns 32-byte array
18
+ * ```
19
+ */
20
+ export function padPassword(password: ByteArray): ByteArray {
21
+ const padded = new Uint8Array(32)
22
+ if (password.length >= 32) {
23
+ padded.set(password.subarray(0, 32))
24
+ } else {
25
+ padded.set(password)
26
+ padded.set(
27
+ DEFAULT_PADDING.subarray(0, 32 - password.length),
28
+ password.length,
29
+ )
30
+ }
31
+ return padded
32
+ }
33
+
34
+ /**
35
+ * Compute the master encryption key for a PDF file.
36
+ *
37
+ * @param password User-supplied password (empty if no password)
38
+ * @param ownerKey Owner key (/O value)
39
+ * @param permissions /P value
40
+ * @param id0 First element of /ID array
41
+ * @param keyLengthBits Usually 40, 128 or 256
42
+ * @param encryptMetadata Whether /EncryptMetadata is false
43
+ */
44
+ export async function computeMasterKey(
45
+ password: ByteArray,
46
+ ownerKey: ByteArray,
47
+ permissions: number,
48
+ id0: ByteArray,
49
+ keyLengthBits: number,
50
+ encryptMetadata: boolean,
51
+ revision: number = 3,
52
+ ): Promise<ByteArray> {
53
+ if (keyLengthBits % 8 !== 0) {
54
+ throw new Error(
55
+ `keyLengthBits must be a multiple of 8, got ${keyLengthBits}`,
56
+ )
57
+ }
58
+
59
+ const keyLengthBytes = keyLengthBits / 8
60
+
61
+ const paddedPassword = padPassword(password)
62
+ const permissionsBytes = int32ToLittleEndianBytes(permissions)
63
+
64
+ const hashInputParts = [paddedPassword, ownerKey, permissionsBytes, id0]
65
+
66
+ if (keyLengthBits > 40 && !encryptMetadata && revision >= 4) {
67
+ hashInputParts.push(new Uint8Array([0xff, 0xff, 0xff, 0xff]))
68
+ }
69
+
70
+ // Initial MD5 hash
71
+ let digest = await md5(concatUint8Arrays(...hashInputParts))
72
+
73
+ if (keyLengthBits > 40) {
74
+ // Perform 50 iterations of MD5 rehash
75
+ for (let i = 0; i < 50; i++) {
76
+ digest = await md5(digest.subarray(0, keyLengthBytes))
77
+ }
78
+ }
79
+
80
+ // Truncate to key length
81
+ return digest.subarray(0, keyLengthBytes)
82
+ }
83
+
84
+ /**
85
+ * Derives an object-specific encryption key from the master key.
86
+ * Used to encrypt individual PDF objects with unique keys.
87
+ *
88
+ * @param mkey - The master encryption key.
89
+ * @param objNumber - The PDF object number.
90
+ * @param objGeneration - The PDF object generation number.
91
+ * @param useAesSalt - Whether to include the AES salt ('sAlT'). Defaults to true.
92
+ * @returns A promise that resolves to the derived object key.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const objectKey = await deriveObjectKey(masterKey, 5, 0)
97
+ * ```
98
+ */
99
+ export async function deriveObjectKey(
100
+ mkey: ByteArray,
101
+ objNumber: number,
102
+ objGeneration: number,
103
+ useAesSalt: boolean = true,
104
+ ): Promise<ByteArray> {
105
+ const extra = useAesSalt ? 4 : 0 // 4 extra bytes if AES salt
106
+ const buffer = new Uint8Array(mkey.length + 5 + extra) // <--- make room for salt if needed
107
+ buffer.set(mkey, 0)
108
+ buffer[mkey.length + 0] = objNumber & 0xff
109
+ buffer[mkey.length + 1] = (objNumber >> 8) & 0xff
110
+ buffer[mkey.length + 2] = (objNumber >> 16) & 0xff
111
+ buffer[mkey.length + 3] = objGeneration & 0xff
112
+ buffer[mkey.length + 4] = (objGeneration >> 8) & 0xff
113
+
114
+ if (useAesSalt) {
115
+ buffer.set([0x73, 0x41, 0x6c, 0x54], mkey.length + 5) // append 'sAlT'
116
+ }
117
+
118
+ const digest = await md5(buffer)
119
+
120
+ const keySize = Math.min(mkey.length + 5, 16)
121
+ return new Uint8Array(digest.subarray(0, keySize))
122
+ }
@@ -0,0 +1,79 @@
1
+ import { computeAlgorithm2bHash } from '../key-derivation/key-derivation-aes256.js'
2
+ import { getRandomBytes } from '../../utils/algos.js'
3
+ import { aes256CbcNoPaddingEncrypt } from '../../utils/algos.js'
4
+ import { ByteArray } from '../../types.js'
5
+
6
+ /**
7
+ * Generates the /U and /UE values for AES-256 PDF encryption.
8
+ * These values are used to validate the user password and decrypt the file key.
9
+ *
10
+ * @param password - The user password.
11
+ * @param fileKey - The 32-byte file encryption key.
12
+ * @returns A promise that resolves to an object containing the 48-byte U and 32-byte UE values.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const { U, UE } = await generateUandUe(password, fileKey)
17
+ * ```
18
+ */
19
+ export async function generateUandUe(
20
+ password: ByteArray,
21
+ fileKey: ByteArray,
22
+ ): Promise<{ U: ByteArray; UE: ByteArray }> {
23
+ const random = getRandomBytes(16)
24
+ const userValidationSalt = random.subarray(0, 8)
25
+ const userKeySalt = random.subarray(8, 16)
26
+
27
+ const U = new Uint8Array(48)
28
+ U.set(await computeAlgorithm2bHash(password, userValidationSalt))
29
+ U.set(userValidationSalt, 32)
30
+ U.set(userKeySalt, 40)
31
+
32
+ const hash = await computeAlgorithm2bHash(password, userKeySalt)
33
+ const UE = await aes256CbcNoPaddingEncrypt(
34
+ hash,
35
+ fileKey,
36
+ new Uint8Array(16),
37
+ )
38
+
39
+ return { U, UE }
40
+ }
41
+
42
+ /**
43
+ * Generates the /O and /OE values for AES-256 PDF encryption.
44
+ * These values are used to validate the owner password and decrypt the file key.
45
+ *
46
+ * @param password - The owner password.
47
+ * @param U - The 48-byte /U value (required for owner key derivation).
48
+ * @param fileKey - The 32-byte file encryption key.
49
+ * @returns A promise that resolves to an object containing the 48-byte O and 32-byte OE values.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const { O, OE } = await generateOandOe(ownerPassword, U, fileKey)
54
+ * ```
55
+ */
56
+ export async function generateOandOe(
57
+ password: ByteArray,
58
+ U: ByteArray,
59
+ fileKey: ByteArray,
60
+ ): Promise<{ O: ByteArray; OE: ByteArray }> {
61
+ const random = getRandomBytes(16)
62
+ const ownerValidationSalt = random.subarray(0, 8)
63
+ const ownerKeySalt = random.subarray(8, 16)
64
+
65
+ const O = new Uint8Array(48)
66
+ O.set(await computeAlgorithm2bHash(password, ownerValidationSalt, U))
67
+ O.set(ownerValidationSalt, 32)
68
+ O.set(ownerKeySalt, 40)
69
+
70
+ const hash = await computeAlgorithm2bHash(password, ownerKeySalt, U)
71
+
72
+ const OE = await aes256CbcNoPaddingEncrypt(
73
+ hash,
74
+ fileKey,
75
+ new Uint8Array(16),
76
+ )
77
+
78
+ return { O, OE }
79
+ }