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,379 @@
1
+ import { PdfDictionary } from '../../core/objects/pdf-dictionary'
2
+ import { PdfName } from '../../core/objects/pdf-name'
3
+ import { PdfNumber } from '../../core/objects/pdf-number'
4
+ import { aes128 } from '../../crypto/ciphers/aes128'
5
+ import { deriveObjectKey } from '../../crypto/key-derivation/key-derivation'
6
+ import { Cipher } from '../../crypto/types'
7
+ import { ByteArray } from '../../types'
8
+ import { assert, assertIfDefined } from '../../utils/assert'
9
+ import { AesV2CryptFilter } from '../crypt-filters/aesv2'
10
+ import { AesV3CryptFilter } from '../crypt-filters/aesv3'
11
+ import { PdfCryptFilter } from '../crypt-filters/base'
12
+ import { IdentityCryptFilter } from '../crypt-filters/identity'
13
+ import { V2CryptFilter } from '../crypt-filters/v2'
14
+ import {
15
+ CryptFilterType,
16
+ PdfCryptFilterDictionary,
17
+ PdfEncryptionAlgorithmType,
18
+ PdfEncryptionDictionary,
19
+ } from '../types'
20
+ import { PdfStandardSecurityHandlerOptions } from './base'
21
+ import { PdfV2SecurityHandler } from './v2'
22
+
23
+ const IDENTITY_CRYPT_FILTER = new IdentityCryptFilter({ authEvent: 'DocOpen' })
24
+
25
+ /**
26
+ * V4 security handler implementing AES-128-CBC encryption with crypt filters.
27
+ * Supports different encryption methods for strings, streams, and embedded files (PDF 1.5).
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const handler = new PdfV4SecurityHandler({
32
+ * password: 'user123',
33
+ * ownerPassword: 'admin456'
34
+ * })
35
+ * handler.setCryptFilter('StmFilter', new AesV2CryptFilter({ authEvent: 'DocOpen' }))
36
+ * ```
37
+ */
38
+ export class PdfV4SecurityHandler extends PdfV2SecurityHandler {
39
+ /** Map of named crypt filters. */
40
+ protected cryptFilters: Map<string, PdfCryptFilter> = new Map([
41
+ ['Identity', IDENTITY_CRYPT_FILTER],
42
+ ])
43
+
44
+ /** Mapping of content types to crypt filter names. */
45
+ protected cryptFiltersByType: {
46
+ [key in CryptFilterType]?: string
47
+ } = {}
48
+
49
+ /**
50
+ * Creates a new V4 security handler with AES-128 encryption.
51
+ *
52
+ * @param options - Configuration options for the handler.
53
+ */
54
+ constructor(options: PdfStandardSecurityHandlerOptions) {
55
+ super(options)
56
+
57
+ this.setCryptFilter(
58
+ 'StdCF',
59
+ new AesV2CryptFilter({
60
+ authEvent: 'DocOpen',
61
+ }),
62
+ )
63
+
64
+ this.setCryptFilterForType('stream', 'StdCF')
65
+ this.setCryptFilterForType('string', 'StdCF')
66
+ }
67
+
68
+ /**
69
+ * Gets a crypt filter by name.
70
+ *
71
+ * @param name - The crypt filter name.
72
+ * @returns The crypt filter, or undefined if not found.
73
+ */
74
+ getCryptFilter(name: string): PdfCryptFilter | undefined {
75
+ return this.cryptFilters.get(name)
76
+ }
77
+
78
+ /**
79
+ * Gets the crypt filter assigned to a content type.
80
+ *
81
+ * @param type - The content type ('string', 'stream', or 'file').
82
+ * @returns The assigned crypt filter, or null if none.
83
+ */
84
+ getCryptFilterByType(type: CryptFilterType): PdfCryptFilter | null {
85
+ const name = this.cryptFiltersByType[type]
86
+ if (name) {
87
+ return this.cryptFilters.get(name) || null
88
+ }
89
+ return null
90
+ }
91
+
92
+ /**
93
+ * Assigns a crypt filter to a content type.
94
+ *
95
+ * @param type - The content type.
96
+ * @param name - The crypt filter name.
97
+ */
98
+ setCryptFilterForType(type: CryptFilterType, name: string): void {
99
+ this.cryptFiltersByType[type] = name
100
+ }
101
+
102
+ /**
103
+ * Registers a named crypt filter.
104
+ *
105
+ * @param name - The filter name.
106
+ * @param filter - The crypt filter instance.
107
+ */
108
+ setCryptFilter(name: string, filter: PdfCryptFilter): void {
109
+ filter.setSecurityHandler(this)
110
+ this.cryptFilters.set(name, filter)
111
+ }
112
+
113
+ /**
114
+ * Gets the encryption revision number.
115
+ *
116
+ * @returns 4 for V4 encryption.
117
+ */
118
+ getRevision(): number {
119
+ return 4
120
+ }
121
+
122
+ /**
123
+ * Gets the encryption version number.
124
+ *
125
+ * @returns 4 for crypt filter-based encryption.
126
+ */
127
+ getVersion(): number {
128
+ return 4
129
+ }
130
+
131
+ /**
132
+ * Computes the object-specific encryption key.
133
+ *
134
+ * @param objectNumber - The PDF object number.
135
+ * @param generationNumber - The PDF generation number.
136
+ * @param algorithm - Optional algorithm type for key derivation.
137
+ * @returns The computed object key.
138
+ * @throws Error if object or generation number is invalid.
139
+ */
140
+ async computeObjectKey(
141
+ objectNumber?: number,
142
+ generationNumber?: number,
143
+ algorithm?: PdfEncryptionAlgorithmType,
144
+ ): Promise<ByteArray> {
145
+ assert(
146
+ objectNumber !== undefined,
147
+ 'Object number is required to derive the key',
148
+ )
149
+ assert(
150
+ generationNumber !== undefined,
151
+ 'Generation number is required to derive the key',
152
+ )
153
+
154
+ assert(objectNumber > 0, 'Object number cannot be zero or negative')
155
+ assert(generationNumber >= 0, 'Generation number cannot be negative')
156
+
157
+ this.masterKey ||= await this.computeMasterKey()
158
+
159
+ const key = await deriveObjectKey(
160
+ this.masterKey,
161
+ objectNumber,
162
+ generationNumber,
163
+ algorithm === 'AES-128-CBC',
164
+ )
165
+
166
+ return key
167
+ }
168
+
169
+ /**
170
+ * Gets an AES-128 cipher for the specified object.
171
+ *
172
+ * @param objectNumber - The PDF object number.
173
+ * @param generationNumber - The PDF generation number.
174
+ * @returns An AES-128 cipher instance.
175
+ */
176
+ protected async getCipher(
177
+ objectNumber?: number,
178
+ generationNumber?: number,
179
+ ): Promise<Cipher> {
180
+ const key = await this.computeObjectKey(objectNumber, generationNumber)
181
+
182
+ return aes128(key)
183
+ }
184
+
185
+ /**
186
+ * Reads encryption parameters and crypt filter definitions from the dictionary.
187
+ *
188
+ * @param encryptionDictionary - The encryption dictionary from the PDF.
189
+ */
190
+ readEncryptionDictionary(
191
+ encryptionDictionary: PdfEncryptionDictionary,
192
+ ): void {
193
+ super.readEncryptionDictionary(encryptionDictionary)
194
+
195
+ if (encryptionDictionary.get('CF')) {
196
+ const cfDict = encryptionDictionary.get('CF')
197
+ assert(cfDict instanceof PdfDictionary, 'CF must be a dictionary')
198
+
199
+ const cfEntries = cfDict.values
200
+
201
+ for (const [key, cf] of Object.entries(cfEntries)) {
202
+ assert(
203
+ cf instanceof PdfDictionary,
204
+ 'Crypt filter must be a dictionary',
205
+ )
206
+
207
+ const cfm = cf.get('CFM')
208
+ const authEvent = cf.get('AuthEvent')
209
+ const length = cf.get('Length')
210
+
211
+ assertIfDefined(
212
+ cfm,
213
+ cfm instanceof PdfName,
214
+ 'CFM must be a name',
215
+ )
216
+ assertIfDefined(
217
+ authEvent,
218
+ authEvent instanceof PdfName,
219
+ 'AuthEvent must be a name',
220
+ )
221
+ assertIfDefined(
222
+ length,
223
+ length instanceof PdfNumber,
224
+ 'Length must be a number',
225
+ )
226
+
227
+ assert(cfm, 'CFM is required in crypt filter dictionary')
228
+ assert(
229
+ authEvent,
230
+ 'AuthEvent is required in crypt filter dictionary',
231
+ )
232
+
233
+ if (cfm.value === 'None') {
234
+ this.setCryptFilter(
235
+ key,
236
+ new IdentityCryptFilter({
237
+ authEvent: authEvent.value,
238
+ }),
239
+ )
240
+ } else if (cfm.value === 'V2') {
241
+ this.setCryptFilter(
242
+ key,
243
+ new V2CryptFilter({
244
+ authEvent: authEvent.value,
245
+ length: length.value,
246
+ }),
247
+ )
248
+ } else if (cfm.value === 'AESV2') {
249
+ this.setCryptFilter(
250
+ key,
251
+ new AesV2CryptFilter({
252
+ authEvent: authEvent.value,
253
+ }),
254
+ )
255
+ } else if (cfm.value === 'AESV3') {
256
+ this.setCryptFilter(
257
+ key,
258
+ new AesV3CryptFilter({
259
+ authEvent: authEvent.value,
260
+ }),
261
+ )
262
+ } else {
263
+ throw new Error(`Unsupported CFM value: ${cfm.value}`)
264
+ }
265
+ }
266
+ }
267
+
268
+ if (encryptionDictionary.values['StmF']) {
269
+ const stmF = encryptionDictionary.values['StmF']
270
+ assert(stmF instanceof PdfName, 'StmF must be a name')
271
+ assert(
272
+ this.cryptFilters.has(stmF.value),
273
+ `StmF references unknown crypt filter: ${stmF.value}`,
274
+ )
275
+ this.setCryptFilterForType('stream', stmF.value)
276
+ }
277
+
278
+ if (encryptionDictionary.values['StrF']) {
279
+ const strF = encryptionDictionary.values['StrF']
280
+ assert(strF instanceof PdfName, 'StrF must be a name')
281
+ assert(
282
+ this.cryptFilters.has(strF.value),
283
+ `StrF references unknown crypt filter: ${strF.value}`,
284
+ )
285
+ this.setCryptFilterForType('string', strF.value)
286
+ }
287
+
288
+ if (encryptionDictionary.values['EFF']) {
289
+ const eff = encryptionDictionary.values['EFF']
290
+ assert(eff instanceof PdfName, 'EFF must be a name')
291
+ assert(
292
+ this.cryptFilters.has(eff.value),
293
+ `EFF references unknown crypt filter: ${eff.value}`,
294
+ )
295
+ this.setCryptFilterForType('file', eff.value)
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Writes the encryption dictionary including crypt filter definitions.
301
+ */
302
+ async write(): Promise<void> {
303
+ await super.write()
304
+
305
+ const dict = this.dict
306
+
307
+ const cfs: Record<string, PdfCryptFilterDictionary> = {}
308
+ this.cryptFilters.forEach((cf, name) => {
309
+ if (cf instanceof IdentityCryptFilter) {
310
+ // The Identity crypt filter is implicit and should not be included
311
+ return
312
+ }
313
+
314
+ cfs[name] = cf.toDictionary()
315
+ })
316
+
317
+ if (Object.keys(cfs).length > 0) {
318
+ dict.set('CF', new PdfDictionary(cfs))
319
+ }
320
+
321
+ if (this.cryptFiltersByType.stream) {
322
+ dict.set('StmF', new PdfName(this.cryptFiltersByType.stream))
323
+ }
324
+
325
+ if (this.cryptFiltersByType.string) {
326
+ dict.set('StrF', new PdfName(this.cryptFiltersByType.string))
327
+ }
328
+
329
+ if (this.cryptFiltersByType.file) {
330
+ dict.set('EFF', new PdfName(this.cryptFiltersByType.file))
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Encrypts data using the appropriate crypt filter for the content type.
336
+ *
337
+ * @param type - The type of content being encrypted.
338
+ * @param data - The data to encrypt.
339
+ * @param objectNumber - The PDF object number.
340
+ * @param generationNumber - The PDF generation number.
341
+ * @returns The encrypted data.
342
+ */
343
+ encrypt(
344
+ type: 'string' | 'stream' | 'file',
345
+ data: ByteArray,
346
+ objectNumber?: number,
347
+ generationNumber?: number,
348
+ ): Promise<ByteArray> {
349
+ const cryptFilter = this.getCryptFilterByType(type)
350
+ if (cryptFilter) {
351
+ return cryptFilter.encrypt(data, objectNumber, generationNumber)
352
+ }
353
+
354
+ return super.encrypt(type, data, objectNumber, generationNumber)
355
+ }
356
+
357
+ /**
358
+ * Decrypts data using the appropriate crypt filter for the content type.
359
+ *
360
+ * @param type - The type of content being decrypted.
361
+ * @param data - The encrypted data.
362
+ * @param objectNumber - The PDF object number.
363
+ * @param generationNumber - The PDF generation number.
364
+ * @returns The decrypted data.
365
+ */
366
+ decrypt(
367
+ type: 'string' | 'stream' | 'file',
368
+ data: ByteArray,
369
+ objectNumber?: number,
370
+ generationNumber?: number,
371
+ ): Promise<ByteArray> {
372
+ const cryptFilter = this.getCryptFilterByType(type)
373
+ if (cryptFilter) {
374
+ return cryptFilter.decrypt(data, objectNumber, generationNumber)
375
+ }
376
+
377
+ return super.decrypt(type, data, objectNumber, generationNumber)
378
+ }
379
+ }
@@ -0,0 +1,298 @@
1
+ import { aes256 } from '../../crypto/ciphers/aes256'
2
+ import { getFileKey } from '../../crypto/key-derivation/key-derivation-aes256'
3
+ import {
4
+ generateOandOe,
5
+ generateUandUe,
6
+ } from '../../crypto/key-gen/key-gen-aes256'
7
+ import { ByteArray } from '../../types'
8
+ import { aes256ecbEncrypt, getRandomBytes } from '../../utils/algos'
9
+ import { assert } from '../../utils/assert'
10
+ import { PdfEncryptionDictionary } from '../types'
11
+ import { PdfV4SecurityHandler } from './v4'
12
+ import { PdfStandardSecurityHandlerOptions } from './base'
13
+ import { AesV3CryptFilter } from '../crypt-filters/aesv3'
14
+ import { PdfHexadecimal } from '../../core/objects/pdf-hexadecimal'
15
+ import { PdfString } from '../../core/objects/pdf-string'
16
+ import { Cipher } from '../../crypto/types'
17
+
18
+ /**
19
+ * V5 security handler implementing AES-256-CBC encryption.
20
+ * This is the most secure encryption method (PDF 2.0).
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const handler = new PdfV5SecurityHandler({
25
+ * password: 'strongPassword123',
26
+ * ownerPassword: 'adminPassword456'
27
+ * })
28
+ * ```
29
+ */
30
+ export class PdfV5SecurityHandler extends PdfV4SecurityHandler {
31
+ /** User encrypted file key (UE value). */
32
+ protected userEncryptedFileKey?: ByteArray
33
+ /** Owner encrypted file key (OE value). */
34
+ protected ownerEncryptedFileKey?: ByteArray
35
+ /** Permissions entry (Perms value). */
36
+ protected perms?: ByteArray
37
+ /** Promise resolving to the file encryption key. */
38
+ protected fileKey?: Promise<ByteArray>
39
+
40
+ /**
41
+ * Creates a new V5 security handler with AES-256 encryption.
42
+ *
43
+ * @param options - Configuration options including optional pre-computed keys.
44
+ */
45
+ constructor(
46
+ options: PdfStandardSecurityHandlerOptions & {
47
+ userEncryptedFileKey?: ByteArray
48
+ ownerEncryptedFileKey?: ByteArray
49
+ },
50
+ ) {
51
+ super(options)
52
+ this.userEncryptedFileKey = options.userEncryptedFileKey
53
+ this.ownerEncryptedFileKey = options.ownerEncryptedFileKey
54
+
55
+ this.setCryptFilter(
56
+ 'StdCF',
57
+ new AesV3CryptFilter({
58
+ authEvent: 'DocOpen',
59
+ }),
60
+ )
61
+
62
+ this.setCryptFilterForType('stream', 'StdCF')
63
+ this.setCryptFilterForType('string', 'StdCF')
64
+ }
65
+
66
+ /**
67
+ * Checks if the handler is ready (has user encrypted file key).
68
+ *
69
+ * @returns True if the handler has the required keys.
70
+ */
71
+ isReady(): boolean {
72
+ return !!this.userEncryptedFileKey
73
+ }
74
+
75
+ /**
76
+ * Initializes encryption keys, either deriving from existing values or generating new ones.
77
+ */
78
+ protected async initKeys(): Promise<void> {
79
+ this.fileKey ||= (async () => {
80
+ if (
81
+ this.userEncryptedFileKey &&
82
+ this.ownerEncryptedFileKey &&
83
+ this.userKey &&
84
+ this.ownerKey &&
85
+ this.perms
86
+ ) {
87
+ const derivedFileKey = await getFileKey(
88
+ this.password,
89
+ this.ownerPassword ?? this.password,
90
+ this.userKey,
91
+ this.userEncryptedFileKey,
92
+ this.ownerKey,
93
+ this.ownerEncryptedFileKey,
94
+ )
95
+
96
+ return derivedFileKey
97
+ }
98
+ const fileKey = getRandomBytes(32)
99
+
100
+ const { U, UE } = await generateUandUe(this.password, fileKey)
101
+
102
+ this.userKey = U
103
+ this.userEncryptedFileKey = UE
104
+
105
+ const { O, OE } = await generateOandOe(
106
+ this.ownerPassword ?? this.password,
107
+ U,
108
+ fileKey,
109
+ )
110
+
111
+ this.ownerKey = O
112
+ this.ownerEncryptedFileKey = OE
113
+
114
+ this.perms = await this.buildPermsEntry(
115
+ this.permissions ?? 0,
116
+ this.encryptMetadata,
117
+ fileKey,
118
+ )
119
+
120
+ return fileKey
121
+ })()
122
+
123
+ await this.fileKey
124
+ }
125
+
126
+ /**
127
+ * Computes the master encryption key.
128
+ *
129
+ * @returns The file encryption key.
130
+ * @throws Error if file key is not initialized.
131
+ */
132
+ protected async computeMasterKey(): Promise<ByteArray> {
133
+ await this.initKeys()
134
+
135
+ if (!this.fileKey) {
136
+ throw new Error('File key not initialized')
137
+ }
138
+
139
+ return this.fileKey
140
+ }
141
+
142
+ /**
143
+ * Builds the Perms entry for the encryption dictionary.
144
+ *
145
+ * @param flags - The permission flags.
146
+ * @param encryptMetadata - Whether metadata is encrypted.
147
+ * @param fileKey - The file encryption key.
148
+ * @returns The encrypted permissions entry.
149
+ */
150
+ private async buildPermsEntry(
151
+ flags: number,
152
+ encryptMetadata: boolean,
153
+ fileKey: ByteArray,
154
+ ): Promise<ByteArray> {
155
+ const block = new Uint8Array(16)
156
+
157
+ // a + b) permissions + 0xFFFFFFFF (little-endian)
158
+ const view = new DataView(block.buffer)
159
+ view.setUint32(0, flags, true) // little-endian
160
+ view.setUint32(4, 0xffffffff, true) // upper 32 bits
161
+
162
+ // c) "T" or "F"
163
+ block[8] = encryptMetadata ? 'T'.charCodeAt(0) : 'F'.charCodeAt(0)
164
+
165
+ // d) "adb"
166
+ block[9] = 'a'.charCodeAt(0)
167
+ block[10] = 'd'.charCodeAt(0)
168
+ block[11] = 'b'.charCodeAt(0)
169
+
170
+ // e) random 4 bytes
171
+ const randomBytes = getRandomBytes(4)
172
+ block.set(randomBytes, 12)
173
+
174
+ return await aes256ecbEncrypt(fileKey, block)
175
+ }
176
+
177
+ /**
178
+ * Gets the encryption key length in bits.
179
+ *
180
+ * @returns 256 for V5 encryption.
181
+ */
182
+ getKeyBits(): number {
183
+ return 256
184
+ }
185
+
186
+ /**
187
+ * Gets an AES-256 cipher.
188
+ *
189
+ * @returns An AES-256 cipher instance.
190
+ */
191
+ protected async getCipher(): Promise<Cipher> {
192
+ this.masterKey ||= await this.computeMasterKey()
193
+
194
+ return aes256(this.masterKey)
195
+ }
196
+
197
+ /**
198
+ * Gets the encryption version number.
199
+ *
200
+ * @returns 5 for AES-256 encryption.
201
+ */
202
+ getVersion(): number {
203
+ return 5
204
+ }
205
+
206
+ /**
207
+ * Gets the encryption revision number.
208
+ *
209
+ * @returns 6 for V5 encryption.
210
+ */
211
+ getRevision(): number {
212
+ return 6
213
+ }
214
+
215
+ /**
216
+ * Writes the encryption dictionary including V5-specific entries.
217
+ *
218
+ * @throws Error if required keys are not computed.
219
+ */
220
+ async write(): Promise<void> {
221
+ await super.write()
222
+ const dict = this.dict
223
+
224
+ assert(this.userEncryptedFileKey, 'User Encrypted File Key not set')
225
+ assert(this.ownerEncryptedFileKey, 'Owner Encrypted File Key not set')
226
+ assert(this.perms, 'Permissions entry not set')
227
+
228
+ dict.set('UE', new PdfString(this.userEncryptedFileKey))
229
+ dict.set('OE', new PdfString(this.ownerEncryptedFileKey))
230
+ dict.set('Perms', new PdfString(this.perms))
231
+ }
232
+
233
+ /**
234
+ * Reads V5-specific encryption parameters from the dictionary.
235
+ *
236
+ * @param encryptionDictionary - The encryption dictionary from the PDF.
237
+ * @throws Error if required entries are missing or invalid.
238
+ */
239
+ readEncryptionDictionary(
240
+ encryptionDictionary: PdfEncryptionDictionary,
241
+ ): void {
242
+ super.readEncryptionDictionary(encryptionDictionary)
243
+
244
+ const { values } = encryptionDictionary
245
+
246
+ assert(
247
+ values['UE'] instanceof PdfHexadecimal ||
248
+ values['UE'] instanceof PdfString,
249
+ 'Invalid or missing /UE entry',
250
+ )
251
+ assert(
252
+ values['OE'] instanceof PdfHexadecimal ||
253
+ values['OE'] instanceof PdfString,
254
+ 'Invalid or missing /OE entry',
255
+ )
256
+ assert(
257
+ values['Perms'] instanceof PdfString ||
258
+ values['Perms'] instanceof PdfHexadecimal,
259
+ 'Invalid or missing /Perms entry',
260
+ )
261
+
262
+ this.userEncryptedFileKey =
263
+ values['UE'] instanceof PdfHexadecimal
264
+ ? values['UE'].bytes
265
+ : values['UE'].raw
266
+ this.ownerEncryptedFileKey =
267
+ values['OE'] instanceof PdfHexadecimal
268
+ ? values['OE'].bytes
269
+ : values['OE'].raw
270
+ this.perms =
271
+ values['Perms'] instanceof PdfHexadecimal
272
+ ? values['Perms'].bytes
273
+ : values['Perms'].raw
274
+ }
275
+
276
+ /**
277
+ * Computes the object encryption key (same as master key for V5).
278
+ *
279
+ * @returns The master encryption key.
280
+ */
281
+ async computeObjectKey(): Promise<ByteArray> {
282
+ return await this.computeMasterKey()
283
+ }
284
+
285
+ /**
286
+ * Recovers the user password from the owner password.
287
+ * Not supported for AES-256 encryption.
288
+ *
289
+ * @throws Error always, as this operation is not supported for V5.
290
+ */
291
+ async recoverUserPassword(
292
+ ownerPassword?: ByteArray | string,
293
+ ): Promise<string> {
294
+ throw new Error(
295
+ 'Recovering user password from owner password is not supported for AES-256',
296
+ )
297
+ }
298
+ }