@awacloud/pdf 0.0.0-stage → 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/CHANGELOG.md +609 -0
- package/LICENSE +661 -0
- package/NOTICE +77 -0
- package/README.md +363 -2
- package/dist/build/index.js +21 -0
- package/dist/build/pdf-full-rw.js +10972 -0
- package/dist/build/pdf-full-rw.meta.json +105 -0
- package/dist/build/pdf-full-rw.min.js +53 -0
- package/dist/build/pdf-full.js +6078 -0
- package/dist/build/pdf-full.meta.json +90 -0
- package/dist/build/pdf-full.min.js +32 -0
- package/dist/build/pdf-large-rw.js +10367 -0
- package/dist/build/pdf-large-rw.meta.json +99 -0
- package/dist/build/pdf-large-rw.min.js +53 -0
- package/dist/build/pdf-large.js +5473 -0
- package/dist/build/pdf-large.meta.json +84 -0
- package/dist/build/pdf-large.min.js +32 -0
- package/dist/build/pdf-legacy-rw.js +12402 -0
- package/dist/build/pdf-legacy-rw.meta.json +110 -0
- package/dist/build/pdf-legacy-rw.min.js +53 -0
- package/dist/build/pdf-legacy.js +7508 -0
- package/dist/build/pdf-legacy.meta.json +95 -0
- package/dist/build/pdf-legacy.min.js +32 -0
- package/dist/build/pdf-rw.js +7578 -0
- package/dist/build/pdf-rw.meta.json +77 -0
- package/dist/build/pdf-rw.min.js +53 -0
- package/dist/build/pdf.js +2684 -0
- package/dist/build/pdf.meta.json +62 -0
- package/dist/build/pdf.min.js +32 -0
- package/dist/standalone/pdf-full-rw.js +16798 -0
- package/dist/standalone/pdf-full-rw.meta.json +78 -0
- package/dist/standalone/pdf-full-rw.min.js +56 -0
- package/dist/standalone/pdf-full.js +11904 -0
- package/dist/standalone/pdf-full.meta.json +63 -0
- package/dist/standalone/pdf-full.min.js +35 -0
- package/dist/standalone/pdf-large-rw.js +16193 -0
- package/dist/standalone/pdf-large-rw.meta.json +72 -0
- package/dist/standalone/pdf-large-rw.min.js +56 -0
- package/dist/standalone/pdf-large.js +11299 -0
- package/dist/standalone/pdf-large.meta.json +57 -0
- package/dist/standalone/pdf-large.min.js +35 -0
- package/dist/standalone/pdf-legacy-rw.js +18228 -0
- package/dist/standalone/pdf-legacy-rw.meta.json +83 -0
- package/dist/standalone/pdf-legacy-rw.min.js +56 -0
- package/dist/standalone/pdf-legacy.js +13334 -0
- package/dist/standalone/pdf-legacy.meta.json +68 -0
- package/dist/standalone/pdf-legacy.min.js +35 -0
- package/dist/standalone/pdf-rw.js +13404 -0
- package/dist/standalone/pdf-rw.meta.json +50 -0
- package/dist/standalone/pdf-rw.min.js +56 -0
- package/dist/standalone/pdf.js +8510 -0
- package/dist/standalone/pdf.meta.json +35 -0
- package/dist/standalone/pdf.min.js +35 -0
- package/docs/README.md +53 -0
- package/docs/api/README.md +38 -0
- package/docs/api/_shared/README.md +91 -0
- package/docs/api/action/README.md +29 -0
- package/docs/api/action/action.md +81 -0
- package/docs/api/action/goTo.md +66 -0
- package/docs/api/action/launch.md +58 -0
- package/docs/api/action/named.md +55 -0
- package/docs/api/action/uri.md +54 -0
- package/docs/api/annot/README.md +53 -0
- package/docs/api/annot/annot.md +114 -0
- package/docs/api/annot/fileAttach.md +53 -0
- package/docs/api/annot/freeText.md +68 -0
- package/docs/api/annot/ink.md +69 -0
- package/docs/api/annot/link.md +74 -0
- package/docs/api/annot/markup.md +83 -0
- package/docs/api/annot/popup.md +52 -0
- package/docs/api/annot/projection.md +56 -0
- package/docs/api/annot/redact.md +67 -0
- package/docs/api/annot/square.md +87 -0
- package/docs/api/annot/stamp.md +54 -0
- package/docs/api/annot/text.md +69 -0
- package/docs/api/annot/widget.md +69 -0
- package/docs/api/associatedFiles/README.md +9 -0
- package/docs/api/associatedFiles/associatedFiles.md +78 -0
- package/docs/api/bundles/README.md +68 -0
- package/docs/api/bundles/dist-matrix.md +165 -0
- package/docs/api/bundles/pdf-full.md +148 -0
- package/docs/api/bundles/pdf-large.md +144 -0
- package/docs/api/bundles/pdf-legacy.md +169 -0
- package/docs/api/content/README.md +29 -0
- package/docs/api/content/color.md +99 -0
- package/docs/api/content/graphics.md +114 -0
- package/docs/api/content/images.md +124 -0
- package/docs/api/content/ops.md +100 -0
- package/docs/api/content/stream.md +107 -0
- package/docs/api/content/text.md +98 -0
- package/docs/api/crypto/README.md +29 -0
- package/docs/api/crypto/aesGcm.md +72 -0
- package/docs/api/crypto/permissions.md +79 -0
- package/docs/api/crypto/security.md +98 -0
- package/docs/api/crypto/standardV4.md +104 -0
- package/docs/api/crypto/standardV5.md +84 -0
- package/docs/api/crypto/standardV6.md +93 -0
- package/docs/api/destination/README.md +9 -0
- package/docs/api/destination/destination.md +79 -0
- package/docs/api/document/README.md +29 -0
- package/docs/api/document/builder.md +281 -0
- package/docs/api/document/catalog.md +98 -0
- package/docs/api/document/document.md +187 -0
- package/docs/api/document/encryptedWriter.md +149 -0
- package/docs/api/document/incrementalWriter.md +148 -0
- package/docs/api/document/page.md +99 -0
- package/docs/api/document/pages.md +82 -0
- package/docs/api/document/resources.md +102 -0
- package/docs/api/document/writer.md +157 -0
- package/docs/api/document/xrefStreamWriter.md +122 -0
- package/docs/api/embedded/README.md +13 -0
- package/docs/api/embedded/collection.md +80 -0
- package/docs/api/embedded/embeddedFile.md +86 -0
- package/docs/api/embedded/fileSpec.md +87 -0
- package/docs/api/errors.md +110 -0
- package/docs/api/extra/3d-richmedia.md +76 -0
- package/docs/api/extra/README.md +99 -0
- package/docs/api/extra/annot-extended.md +71 -0
- package/docs/api/extra/associated-files.md +70 -0
- package/docs/api/extra/ccitt-fax-decoder.md +74 -0
- package/docs/api/extra/color-spaces-extended.md +72 -0
- package/docs/api/extra/content-ops-extended.md +82 -0
- package/docs/api/extra/document-parts.md +69 -0
- package/docs/api/extra/embedded-files-portfolio.md +87 -0
- package/docs/api/extra/font-cid-typed.md +77 -0
- package/docs/api/extra/font-color-tagging.md +76 -0
- package/docs/api/extra/form-actions-extended.md +75 -0
- package/docs/api/extra/info-dict-deprecated.md +72 -0
- package/docs/api/extra/jbig2-read.md +80 -0
- package/docs/api/extra/legacy-deprecated-annots.md +89 -0
- package/docs/api/extra/legacy-deprecated-filters.md +78 -0
- package/docs/api/extra/legacy-rc4-read.md +74 -0
- package/docs/api/extra/legacy-xfa-read.md +65 -0
- package/docs/api/extra/linearization-write.md +71 -0
- package/docs/api/extra/misc.md +93 -0
- package/docs/api/extra/optional-content-extended.md +83 -0
- package/docs/api/extra/pdf-a-output-intent.md +65 -0
- package/docs/api/extra/pdf-sandbox.md +76 -0
- package/docs/api/extra/pdf-ua-tagged.md +63 -0
- package/docs/api/extra/pdf-x-prepress.md +65 -0
- package/docs/api/extra/redaction-iso32005.md +65 -0
- package/docs/api/extra/shading-typed.md +73 -0
- package/docs/api/extra/sig-aes-gcm.md +69 -0
- package/docs/api/extra/sig-pades.md +103 -0
- package/docs/api/extra/tagged-pdf-typed.md +78 -0
- package/docs/api/extra/transparency-typed.md +74 -0
- package/docs/api/extra/well-tagged-pdf.md +61 -0
- package/docs/api/extra/xmp-extended.md +65 -0
- package/docs/api/font/README.md +25 -0
- package/docs/api/font/embed.md +157 -0
- package/docs/api/font/encoding.md +95 -0
- package/docs/api/font/font.md +97 -0
- package/docs/api/font/type3.md +89 -0
- package/docs/api/form/README.md +35 -0
- package/docs/api/form/acroform.md +88 -0
- package/docs/api/form/appearance.md +87 -0
- package/docs/api/form/button.md +97 -0
- package/docs/api/form/choice.md +96 -0
- package/docs/api/form/fieldTree.md +93 -0
- package/docs/api/form/signature.md +90 -0
- package/docs/api/form/text.md +88 -0
- package/docs/api/linearization/README.md +11 -0
- package/docs/api/linearization/linearization.md +81 -0
- package/docs/api/main.md +116 -0
- package/docs/api/metadata/README.md +10 -0
- package/docs/api/metadata/info.md +70 -0
- package/docs/api/metadata/xmp.md +62 -0
- package/docs/api/ocg/README.md +23 -0
- package/docs/api/ocg/config.md +95 -0
- package/docs/api/ocg/ocg.md +77 -0
- package/docs/api/outline/README.md +11 -0
- package/docs/api/outline/outline.md +107 -0
- package/docs/api/pdf.md +152 -0
- package/docs/api/prepress/README.md +10 -0
- package/docs/api/prepress/outputIntent.md +79 -0
- package/docs/api/prepress/pageBoundary.md +75 -0
- package/docs/api/sig/README.md +32 -0
- package/docs/api/sig/byteRange.md +120 -0
- package/docs/api/sig/certChain.md +84 -0
- package/docs/api/sig/dss.md +111 -0
- package/docs/api/sig/oids.md +76 -0
- package/docs/api/sig/sha1.md +72 -0
- package/docs/api/sig/sign.md +317 -0
- package/docs/api/sig/signature.md +178 -0
- package/docs/api/sig/timestamp.md +84 -0
- package/docs/api/syntax/README.md +29 -0
- package/docs/api/syntax/crossRefStream.md +115 -0
- package/docs/api/syntax/filters/README.md +50 -0
- package/docs/api/syntax/filters/ascii85.md +76 -0
- package/docs/api/syntax/filters/asciiHex.md +73 -0
- package/docs/api/syntax/filters/dispatch.md +125 -0
- package/docs/api/syntax/filters/flate.md +134 -0
- package/docs/api/syntax/filters/runLength.md +78 -0
- package/docs/api/syntax/objStream.md +88 -0
- package/docs/api/syntax/parser-obj.md +97 -0
- package/docs/api/syntax/parser.md +151 -0
- package/docs/api/syntax/serializer.md +109 -0
- package/docs/api/syntax/tokenizer.md +104 -0
- package/docs/api/syntax/trailer.md +85 -0
- package/docs/api/syntax/xref.md +139 -0
- package/docs/api/tagged/README.md +25 -0
- package/docs/api/tagged/classMap.md +67 -0
- package/docs/api/tagged/markedContent.md +62 -0
- package/docs/api/tagged/parentTree.md +67 -0
- package/docs/api/tagged/roleMap.md +67 -0
- package/docs/api/tagged/structElement.md +76 -0
- package/docs/api/tagged/structTree.md +75 -0
- package/docs/guide/coverage.md +113 -0
- package/docs/guide/crypto.md +121 -0
- package/docs/guide/extending.md +76 -0
- package/docs/guide/getting-started.md +75 -0
- package/docs/guide/legacy-1.7.md +42 -0
- package/docs/guide/pades-integration.md +579 -0
- package/docs/guide/read-pdf.md +89 -0
- package/package.json +97 -4
- package/src/_shared/index.js +179 -0
- package/src/action/action.js +119 -0
- package/src/action/goTo.js +89 -0
- package/src/action/launch.js +61 -0
- package/src/action/named.js +54 -0
- package/src/action/uri.js +51 -0
- package/src/annot/annot.js +212 -0
- package/src/annot/fileAttach.js +55 -0
- package/src/annot/freeText.js +82 -0
- package/src/annot/ink.js +77 -0
- package/src/annot/link.js +77 -0
- package/src/annot/markup.js +91 -0
- package/src/annot/popup.js +53 -0
- package/src/annot/projection.js +52 -0
- package/src/annot/redact.js +87 -0
- package/src/annot/square.js +132 -0
- package/src/annot/stamp.js +48 -0
- package/src/annot/text.js +54 -0
- package/src/annot/widget.js +61 -0
- package/src/associatedFiles/associatedFiles.js +86 -0
- package/src/bundles/pdf-full.js +91 -0
- package/src/bundles/pdf-large.js +81 -0
- package/src/bundles/pdf-legacy.js +107 -0
- package/src/content/color.js +114 -0
- package/src/content/graphics.js +192 -0
- package/src/content/images.js +160 -0
- package/src/content/ops.js +137 -0
- package/src/content/stream.js +154 -0
- package/src/content/text.js +125 -0
- package/src/crypto/aesGcm.js +123 -0
- package/src/crypto/permissions.js +112 -0
- package/src/crypto/security.js +327 -0
- package/src/crypto/standardV4.js +443 -0
- package/src/crypto/standardV5.js +306 -0
- package/src/crypto/standardV6.js +334 -0
- package/src/destination/destination.js +183 -0
- package/src/document/builder.js +618 -0
- package/src/document/catalog.js +100 -0
- package/src/document/document.js +472 -0
- package/src/document/encryptedWriter.js +554 -0
- package/src/document/incrementalWriter.js +514 -0
- package/src/document/page.js +131 -0
- package/src/document/pages.js +103 -0
- package/src/document/resources.js +146 -0
- package/src/document/writer.js +211 -0
- package/src/document/xrefStreamWriter.js +353 -0
- package/src/embedded/collection.js +102 -0
- package/src/embedded/embeddedFile.js +99 -0
- package/src/embedded/fileSpec.js +137 -0
- package/src/errors.js +78 -0
- package/src/extra/3d-richmedia.js +171 -0
- package/src/extra/annot-extended.js +200 -0
- package/src/extra/associated-files.js +131 -0
- package/src/extra/ccitt-fax-decoder.js +776 -0
- package/src/extra/color-spaces-extended.js +196 -0
- package/src/extra/content-ops-extended.js +153 -0
- package/src/extra/document-parts.js +149 -0
- package/src/extra/embedded-files-portfolio.js +234 -0
- package/src/extra/font-cid-typed.js +185 -0
- package/src/extra/font-color-tagging.js +144 -0
- package/src/extra/form-actions-extended.js +196 -0
- package/src/extra/info-dict-deprecated.js +137 -0
- package/src/extra/jbig2-read.js +169 -0
- package/src/extra/legacy-deprecated-annots.js +198 -0
- package/src/extra/legacy-deprecated-filters.js +167 -0
- package/src/extra/legacy-rc4-read.js +235 -0
- package/src/extra/legacy-xfa-read.js +104 -0
- package/src/extra/linearization-write.js +97 -0
- package/src/extra/misc.js +217 -0
- package/src/extra/optional-content-extended.js +142 -0
- package/src/extra/pdf-a-output-intent.js +112 -0
- package/src/extra/pdf-sandbox.js +88 -0
- package/src/extra/pdf-ua-tagged.js +116 -0
- package/src/extra/pdf-x-prepress.js +114 -0
- package/src/extra/redaction-iso32005.js +136 -0
- package/src/extra/shading-typed.js +222 -0
- package/src/extra/sig-aes-gcm.js +135 -0
- package/src/extra/sig-pades.js +242 -0
- package/src/extra/tagged-pdf-typed.js +203 -0
- package/src/extra/transparency-typed.js +135 -0
- package/src/extra/well-tagged-pdf.js +138 -0
- package/src/extra/xmp-extended.js +190 -0
- package/src/font/embed.js +480 -0
- package/src/font/encoding.js +92 -0
- package/src/font/font.js +101 -0
- package/src/font/type3.js +75 -0
- package/src/form/acroform.js +94 -0
- package/src/form/appearance.js +90 -0
- package/src/form/button.js +105 -0
- package/src/form/choice.js +152 -0
- package/src/form/fieldTree.js +120 -0
- package/src/form/signature.js +100 -0
- package/src/form/text.js +101 -0
- package/src/linearization/linearization.js +107 -0
- package/src/main.js +411 -0
- package/src/metadata/info.js +87 -0
- package/src/metadata/xmp.js +62 -0
- package/src/ocg/config.js +156 -0
- package/src/ocg/ocg.js +124 -0
- package/src/outline/outline.js +157 -0
- package/src/pdf.js +133 -0
- package/src/prepress/outputIntent.js +118 -0
- package/src/prepress/pageBoundary.js +108 -0
- package/src/sig/byteRange.js +306 -0
- package/src/sig/certChain.js +247 -0
- package/src/sig/dss.js +317 -0
- package/src/sig/oids.js +157 -0
- package/src/sig/sha1.js +142 -0
- package/src/sig/sign.js +1899 -0
- package/src/sig/signature.js +1441 -0
- package/src/sig/timestamp.js +236 -0
- package/src/syntax/crossRefStream.js +133 -0
- package/src/syntax/filters/ascii85.js +122 -0
- package/src/syntax/filters/asciiHex.js +83 -0
- package/src/syntax/filters/dispatch.js +176 -0
- package/src/syntax/filters/flate.js +316 -0
- package/src/syntax/filters/runLength.js +96 -0
- package/src/syntax/objStream.js +99 -0
- package/src/syntax/parser-obj.js +52 -0
- package/src/syntax/parser.js +321 -0
- package/src/syntax/serializer.js +221 -0
- package/src/syntax/tokenizer.js +290 -0
- package/src/syntax/trailer.js +76 -0
- package/src/syntax/xref.js +341 -0
- package/src/tagged/classMap.js +81 -0
- package/src/tagged/markedContent.js +123 -0
- package/src/tagged/parentTree.js +126 -0
- package/src/tagged/roleMap.js +107 -0
- package/src/tagged/structElement.js +138 -0
- package/src/tagged/structTree.js +94 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// Copyright (c) 2026 AwaCloud SAS
|
|
2
|
+
// Author: Matthieu Bouilloux
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
// Dual-licensed; see the NOTICE file for licensing and any additional terms.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview Encrypted Document writer (V=4/5/6 handlers, AES-GCM).
|
|
8
|
+
*
|
|
9
|
+
* Wraps `pdfWriter.writeDocument` with an `encrypt` path:
|
|
10
|
+
*
|
|
11
|
+
* - V=4, R=4 — PDF 1.6 Standard Security Handler. method:
|
|
12
|
+
* 'AESV2' (AES-128 CBC) or 'V2' (RC4-128). Per-object key
|
|
13
|
+
* derivation (MD5 of FEK || objId || gen [+ 'sAlT' for AESV2]).
|
|
14
|
+
* - V=5, R=5 — Adobe Extension Level 3 (AES-256-CBC, SHA-256, no
|
|
15
|
+
* hardening loop).
|
|
16
|
+
* - V=5, R=6 — ISO 32000-2:2020 (AES-256-CBC, Algorithm 2.B
|
|
17
|
+
* hardening, SHA-256/384/512). With method 'AESV4' an ISO/TS
|
|
18
|
+
* 32003 AES-256-GCM crypt filter is emitted (IV || ct || tag).
|
|
19
|
+
*
|
|
20
|
+
* Strategy:
|
|
21
|
+
*
|
|
22
|
+
* 1. Derive a 32-byte file encryption key (FEK) from `randomBytes`
|
|
23
|
+
* (caller-supplied for determinism in tests).
|
|
24
|
+
* 2. Build `/O`, `/U`, `/OE`, `/UE` via the per-revision helpers
|
|
25
|
+
* exposed by `pdfStandardV5` / `pdfStandardV6` (`buildUUE`,
|
|
26
|
+
* `buildOOE`).
|
|
27
|
+
* 3. Build `/Perms` (Algorithm 10) carrying `P` + `EncryptMetadata`.
|
|
28
|
+
* 4. Walk the indirect graph, encrypt every `string` (literal or
|
|
29
|
+
* hex) and every `stream` `.raw` payload in place — each with a
|
|
30
|
+
* fresh 16-byte IV — producing a new graph.
|
|
31
|
+
* 5. Append an `/Encrypt` indirect (last object number) to the graph.
|
|
32
|
+
* 6. Call `pdfWriter.writeDocument({ indirects, root, info, id })`
|
|
33
|
+
* with `id` matching the one baked into the FEK derivation when
|
|
34
|
+
* relevant (V=5 ignores /ID in key derivation per the spec).
|
|
35
|
+
*
|
|
36
|
+
* @module pdf/document/encryptedWriter
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Module factory — worker-safe, self-contained.
|
|
41
|
+
*/
|
|
42
|
+
import { pdfErrors } from '../errors.js';
|
|
43
|
+
import { pdfWriter } from './writer.js';
|
|
44
|
+
import { pdfStandardV5 } from '../crypto/standardV5.js';
|
|
45
|
+
import { pdfStandardV6 } from '../crypto/standardV6.js';
|
|
46
|
+
import { pdfStandardV4 } from '../crypto/standardV4.js';
|
|
47
|
+
import { pdfAesGcm } from '../crypto/aesGcm.js';
|
|
48
|
+
|
|
49
|
+
export const pdfEncryptedWriter = {
|
|
50
|
+
name: 'pdfEncryptedWriter',
|
|
51
|
+
dependencies: [
|
|
52
|
+
'pdfErrors',
|
|
53
|
+
'pdfWriter',
|
|
54
|
+
'pdfStandardV5',
|
|
55
|
+
'pdfStandardV6',
|
|
56
|
+
'pdfStandardV4',
|
|
57
|
+
'pdfAesGcm'
|
|
58
|
+
],
|
|
59
|
+
deps: [pdfErrors, pdfWriter, pdfStandardV5, pdfStandardV6, pdfStandardV4, pdfAesGcm],
|
|
60
|
+
factory(errors, writerMod, v5Mod, v6Mod, v4Mod, gcmMod) {
|
|
61
|
+
const { EncryptionError, RenderError } = errors;
|
|
62
|
+
const writeDocument = writerMod && writerMod.writeDocument;
|
|
63
|
+
|
|
64
|
+
if (typeof writeDocument !== 'function') {
|
|
65
|
+
throw new EncryptionError('pdf/crypto/enc-writer/missing-writer',
|
|
66
|
+
'pdfEncryptedWriter requires pdfWriter.writeDocument');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cryptographic randomness only: `enc.randomBytes`, else
|
|
70
|
+
// `globalThis.crypto.getRandomValues` (read per call), else a typed refusal.
|
|
71
|
+
function resolveRandomBytes(enc) {
|
|
72
|
+
if (enc.randomBytes) return enc.randomBytes;
|
|
73
|
+
if (globalThis.crypto
|
|
74
|
+
&& typeof globalThis.crypto.getRandomValues === 'function') {
|
|
75
|
+
return (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
76
|
+
}
|
|
77
|
+
throw new EncryptionError('pdf/crypto/enc-writer/no-random',
|
|
78
|
+
'no cryptographic random source: pass opts.encrypt.randomBytes '
|
|
79
|
+
+ '(crypto.getRandomValues is unavailable)');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function selectHandler(version, revision) {
|
|
83
|
+
if (version === 5 && revision === 5) return v5Mod;
|
|
84
|
+
if (version === 5 && revision === 6) return v6Mod;
|
|
85
|
+
if (version === 4 && revision === 4) {
|
|
86
|
+
if (!v4Mod) {
|
|
87
|
+
throw new EncryptionError('pdf/crypto/enc-writer/missing-v4',
|
|
88
|
+
'V=4 requires pdfStandardV4');
|
|
89
|
+
}
|
|
90
|
+
return v4Mod;
|
|
91
|
+
}
|
|
92
|
+
throw new EncryptionError('pdf/crypto/enc-writer/unsupported-version',
|
|
93
|
+
'encrypted write supports V=4 R=4, V=5 R=5, or V=5 R=6',
|
|
94
|
+
{ context: { version, revision } });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function bytesToObj(bytes) {
|
|
98
|
+
// Produce a typed PDF string carrying raw bytes; the
|
|
99
|
+
// serializer will pick a representation (hex preferred
|
|
100
|
+
// when bytes are non-printable, which is the case for
|
|
101
|
+
// AES-256 ciphertext).
|
|
102
|
+
return { type: 'string', value: bytes, syntax: 'hex' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function intObj(n) { return { type: 'int', value: n | 0 }; }
|
|
106
|
+
function nameObj(s) { return { type: 'name', value: s }; }
|
|
107
|
+
function boolObj(b) { return { type: 'bool', value: !!b }; }
|
|
108
|
+
|
|
109
|
+
// Detect /Type /EmbeddedFile streams — used by the EFF dispatch
|
|
110
|
+
// path so we can route through `encryptEmbeddedFile` with the
|
|
111
|
+
// /EFF-resolved method instead of `encryptStream` with /StmF.
|
|
112
|
+
// Mirrors `pdfSecurity.isEmbeddedFileStream` (read side,
|
|
113
|
+
// Follow-up J).
|
|
114
|
+
function isEmbeddedFileStream(value) {
|
|
115
|
+
if (!value || value.type !== 'stream') return false;
|
|
116
|
+
const d = value.dict;
|
|
117
|
+
if (!d || d.type !== 'dict' || !d.entries) return false;
|
|
118
|
+
const t = d.entries.Type;
|
|
119
|
+
if (!t) return false;
|
|
120
|
+
if (t.type === 'name') return t.value === 'EmbeddedFile';
|
|
121
|
+
if (typeof t === 'string') return t === 'EmbeddedFile';
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function encryptObjectInPlace(value, encryptOne, encryptEmbedded) {
|
|
126
|
+
// Recursively walk a typed object graph, encrypting
|
|
127
|
+
// strings and stream payloads with `encryptOne(bytes)`.
|
|
128
|
+
// When `encryptEmbedded` is provided AND a stream is
|
|
129
|
+
// detected as /Type /EmbeddedFile, the stream payload is
|
|
130
|
+
// routed via `encryptEmbedded(bytes)` instead — used by
|
|
131
|
+
// the EFF write dispatch (Follow-up O, ISO 32000-1 §7.6.5).
|
|
132
|
+
if (!value || typeof value !== 'object') return value;
|
|
133
|
+
switch (value.type) {
|
|
134
|
+
case 'string': {
|
|
135
|
+
const ct = encryptOne(value.value);
|
|
136
|
+
return { type: 'string', value: ct, syntax: 'hex' };
|
|
137
|
+
}
|
|
138
|
+
case 'array': {
|
|
139
|
+
const items = new Array(value.items.length);
|
|
140
|
+
for (let i = 0; i < value.items.length; i++) {
|
|
141
|
+
items[i] = encryptObjectInPlace(
|
|
142
|
+
value.items[i], encryptOne, encryptEmbedded);
|
|
143
|
+
}
|
|
144
|
+
return { type: 'array', items };
|
|
145
|
+
}
|
|
146
|
+
case 'dict': {
|
|
147
|
+
const entries = {};
|
|
148
|
+
for (const k of Object.keys(value.entries)) {
|
|
149
|
+
entries[k] = encryptObjectInPlace(
|
|
150
|
+
value.entries[k], encryptOne, encryptEmbedded);
|
|
151
|
+
}
|
|
152
|
+
return { type: 'dict', entries };
|
|
153
|
+
}
|
|
154
|
+
case 'stream': {
|
|
155
|
+
const dict = encryptObjectInPlace(
|
|
156
|
+
value.dict, encryptOne, encryptEmbedded);
|
|
157
|
+
const useEff = encryptEmbedded
|
|
158
|
+
&& isEmbeddedFileStream(value);
|
|
159
|
+
const enc = useEff ? encryptEmbedded : encryptOne;
|
|
160
|
+
const raw = value.raw instanceof Uint8Array
|
|
161
|
+
? enc(value.raw)
|
|
162
|
+
: value.raw;
|
|
163
|
+
return { type: 'stream', dict, raw };
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildEncryptDict(meta) {
|
|
171
|
+
const e = {
|
|
172
|
+
Filter: nameObj('Standard'),
|
|
173
|
+
V: intObj(meta.V),
|
|
174
|
+
R: intObj(meta.R),
|
|
175
|
+
Length: intObj(meta.keyBits),
|
|
176
|
+
P: intObj(meta.P),
|
|
177
|
+
O: bytesToObj(meta.O),
|
|
178
|
+
U: bytesToObj(meta.U),
|
|
179
|
+
EncryptMetadata: boolObj(meta.encryptMetadata !== false)
|
|
180
|
+
};
|
|
181
|
+
if (meta.OE) e.OE = bytesToObj(meta.OE);
|
|
182
|
+
if (meta.UE) e.UE = bytesToObj(meta.UE);
|
|
183
|
+
if (meta.Perms) e.Perms = bytesToObj(meta.Perms);
|
|
184
|
+
|
|
185
|
+
// /CF — crypt filters. /StdCF is the default for /StmF and
|
|
186
|
+
// /StrF. When `effCfm` is distinct from `cfm`, an
|
|
187
|
+
// additional `/StdEFF` crypt filter is emitted and /EFF
|
|
188
|
+
// names it; otherwise /EFF falls back to /StdCF.
|
|
189
|
+
//
|
|
190
|
+
// CFM values:
|
|
191
|
+
// - V=4 + 'AESV2' → CFM=AESV2 (AES-128 CBC), Length=16
|
|
192
|
+
// - V=4 + 'V2' → CFM=V2 (RC4-128), Length=16
|
|
193
|
+
// - V=5 + 'AESV3' → CFM=AESV3 (AES-256 CBC), Length=32
|
|
194
|
+
// - V=5 + 'AESV4' → CFM=AESV4 (AES-256 GCM), Length=32
|
|
195
|
+
const cfm = meta.cfm;
|
|
196
|
+
const cfLength = meta.cfLength;
|
|
197
|
+
const cfEntries = {
|
|
198
|
+
StdCF: {
|
|
199
|
+
type: 'dict',
|
|
200
|
+
entries: {
|
|
201
|
+
CFM: nameObj(cfm),
|
|
202
|
+
AuthEvent: nameObj('DocOpen'),
|
|
203
|
+
Length: intObj(cfLength)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const effCfm = meta.effCfm;
|
|
208
|
+
const useDistinctEff = effCfm && effCfm !== cfm;
|
|
209
|
+
if (useDistinctEff) {
|
|
210
|
+
cfEntries.StdEFF = {
|
|
211
|
+
type: 'dict',
|
|
212
|
+
entries: {
|
|
213
|
+
CFM: nameObj(effCfm),
|
|
214
|
+
AuthEvent: nameObj('DocOpen'),
|
|
215
|
+
Length: intObj(meta.effCfLength | 0 || cfLength)
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
e.CF = { type: 'dict', entries: cfEntries };
|
|
220
|
+
e.StmF = nameObj('StdCF');
|
|
221
|
+
e.StrF = nameObj('StdCF');
|
|
222
|
+
// /EFF (ISO 32000-1 §7.6.5) names the crypt filter used for
|
|
223
|
+
// /Type /EmbeddedFile streams. When unspecified, readers
|
|
224
|
+
// fall back to /StmF — we keep the field omitted in that
|
|
225
|
+
// case to match the prior behaviour.
|
|
226
|
+
if (useDistinctEff) {
|
|
227
|
+
e.EFF = nameObj('StdEFF');
|
|
228
|
+
}
|
|
229
|
+
return { type: 'dict', entries: e };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function nextObjNum(indirects) {
|
|
233
|
+
let max = 0;
|
|
234
|
+
for (const it of indirects) {
|
|
235
|
+
if (it && Number.isFinite(it.num) && it.num > max) max = it.num;
|
|
236
|
+
}
|
|
237
|
+
return max + 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* writeEncryptedDocument(opts) — produce a /Encrypt-protected
|
|
242
|
+
* PDF.
|
|
243
|
+
*
|
|
244
|
+
* opts mirrors `writeDocument` plus:
|
|
245
|
+
*
|
|
246
|
+
* encrypt: {
|
|
247
|
+
* version: 5,
|
|
248
|
+
* revision: 5 | 6,
|
|
249
|
+
* keyBits: 256,
|
|
250
|
+
* ownerPassword: string | Uint8Array,
|
|
251
|
+
* userPassword: string | Uint8Array,
|
|
252
|
+
* permissions: number, // /P bitmask (Table 24)
|
|
253
|
+
* encryptMetadata?: boolean,
|
|
254
|
+
* randomBytes?: (n) => Uint8Array
|
|
255
|
+
* // default `globalThis.crypto.getRandomValues`; when
|
|
256
|
+
* // neither exists the call throws
|
|
257
|
+
* // `pdf/crypto/enc-writer/no-random`
|
|
258
|
+
* }
|
|
259
|
+
*
|
|
260
|
+
* Returns { bytes, encryptObjNum, fek, id }.
|
|
261
|
+
*/
|
|
262
|
+
function writeEncryptedDocument(opts) {
|
|
263
|
+
if (!opts || typeof opts !== 'object') {
|
|
264
|
+
throw new RenderError('pdf/crypto/enc-writer/bad-input',
|
|
265
|
+
'writeEncryptedDocument requires opts');
|
|
266
|
+
}
|
|
267
|
+
const enc = opts.encrypt;
|
|
268
|
+
if (!enc || typeof enc !== 'object') {
|
|
269
|
+
throw new RenderError('pdf/crypto/enc-writer/no-encrypt',
|
|
270
|
+
'opts.encrypt is required');
|
|
271
|
+
}
|
|
272
|
+
const version = enc.version | 0;
|
|
273
|
+
const revision = enc.revision | 0;
|
|
274
|
+
const handler = selectHandler(version, revision);
|
|
275
|
+
const rand = resolveRandomBytes(enc);
|
|
276
|
+
const encryptMetadata = enc.encryptMetadata !== false;
|
|
277
|
+
const P = enc.permissions | 0;
|
|
278
|
+
|
|
279
|
+
// Resolve method + key size per (version, revision).
|
|
280
|
+
// Default method:
|
|
281
|
+
// - V=4 R=4 → 'AESV2' (modern V=4 default)
|
|
282
|
+
// - V=5 R=5/6 → 'AESV3' (CBC) ; opt-in 'AESV4' (GCM)
|
|
283
|
+
let method = enc.method;
|
|
284
|
+
if (!method) method = (version === 4) ? 'AESV2' : 'AESV3';
|
|
285
|
+
|
|
286
|
+
// Validate method against (version, revision).
|
|
287
|
+
if (version === 4 && method !== 'AESV2' && method !== 'V2') {
|
|
288
|
+
throw new RenderError('pdf/crypto/enc-writer/bad-method',
|
|
289
|
+
'V=4 method must be AESV2 or V2',
|
|
290
|
+
{ context: { method } });
|
|
291
|
+
}
|
|
292
|
+
if (version === 5 && method !== 'AESV3' && method !== 'AESV4') {
|
|
293
|
+
throw new RenderError('pdf/crypto/enc-writer/bad-method',
|
|
294
|
+
'V=5 method must be AESV3 or AESV4',
|
|
295
|
+
{ context: { method } });
|
|
296
|
+
}
|
|
297
|
+
if (method === 'AESV4' && !gcmMod) {
|
|
298
|
+
throw new EncryptionError('pdf/crypto/enc-writer/missing-gcm',
|
|
299
|
+
'AESV4 (GCM) requires pdfAesGcm');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Optional /EFF method — distinct cipher for /Type
|
|
303
|
+
// /EmbeddedFile streams (ISO 32000-1 §7.6.5, Follow-up O).
|
|
304
|
+
// When set, the writer dispatches embedded-file streams
|
|
305
|
+
// through `handler.encryptEmbeddedFile` (V=4) or the GCM
|
|
306
|
+
// wrapper (V=5/AESV4) with the EFF method, while regular
|
|
307
|
+
// streams stay on /StmF. Validated against (V, R) using the
|
|
308
|
+
// same rules as `method`.
|
|
309
|
+
const effMethod = enc.effMethod || null;
|
|
310
|
+
if (effMethod) {
|
|
311
|
+
if (version === 4
|
|
312
|
+
&& effMethod !== 'AESV2' && effMethod !== 'V2') {
|
|
313
|
+
throw new RenderError('pdf/crypto/enc-writer/bad-eff-method',
|
|
314
|
+
'V=4 effMethod must be AESV2 or V2',
|
|
315
|
+
{ context: { effMethod } });
|
|
316
|
+
}
|
|
317
|
+
if (version === 5
|
|
318
|
+
&& effMethod !== 'AESV3' && effMethod !== 'AESV4') {
|
|
319
|
+
throw new RenderError('pdf/crypto/enc-writer/bad-eff-method',
|
|
320
|
+
'V=5 effMethod must be AESV3 or AESV4',
|
|
321
|
+
{ context: { effMethod } });
|
|
322
|
+
}
|
|
323
|
+
if (effMethod === 'AESV4' && !gcmMod) {
|
|
324
|
+
throw new EncryptionError('pdf/crypto/enc-writer/missing-gcm',
|
|
325
|
+
'AESV4 (GCM) effMethod requires pdfAesGcm');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const fekBytes = (version === 4) ? 16 : 32;
|
|
330
|
+
const keyBits = enc.keyBits | 0 || (fekBytes * 8);
|
|
331
|
+
|
|
332
|
+
// id is required for V=4 key derivation (and PDF mandates
|
|
333
|
+
// /ID when /Encrypt is present). Produce it now.
|
|
334
|
+
let id = opts.id;
|
|
335
|
+
if (!id) {
|
|
336
|
+
const a = rand(16);
|
|
337
|
+
const b = rand(16);
|
|
338
|
+
id = [a, b];
|
|
339
|
+
}
|
|
340
|
+
const idFirst = id[0] instanceof Uint8Array ? id[0] : new Uint8Array(id[0]);
|
|
341
|
+
|
|
342
|
+
// Step 1 — fresh FEK + build /O, /U (+ /OE, /UE, /Perms for V=5).
|
|
343
|
+
let fek, O, U, OE, UE, Perms;
|
|
344
|
+
if (version === 4) {
|
|
345
|
+
const ou = handler.buildOU(
|
|
346
|
+
enc.ownerPassword, enc.userPassword, P, idFirst, encryptMetadata);
|
|
347
|
+
O = ou.O;
|
|
348
|
+
U = ou.U;
|
|
349
|
+
fek = ou.fek;
|
|
350
|
+
OE = null; UE = null; Perms = null;
|
|
351
|
+
} else {
|
|
352
|
+
fek = rand(32);
|
|
353
|
+
const uValSalt = rand(8);
|
|
354
|
+
const uKeySalt = rand(8);
|
|
355
|
+
const oValSalt = rand(8);
|
|
356
|
+
const oKeySalt = rand(8);
|
|
357
|
+
const u = handler.buildUUE(enc.userPassword, fek, uValSalt, uKeySalt);
|
|
358
|
+
U = u.U; UE = u.UE;
|
|
359
|
+
const o = handler.buildOOE(enc.ownerPassword, fek, U, oValSalt, oKeySalt);
|
|
360
|
+
O = o.O; OE = o.OE;
|
|
361
|
+
Perms = handler.buildPerms(P, fek, encryptMetadata, rand);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Step 2 — encrypt strings/streams in the graph.
|
|
365
|
+
// Per-object encryption requires (objNum, gen) for V=4.
|
|
366
|
+
// Two factories produced per object — one for /StmF (regular
|
|
367
|
+
// streams + strings), one for /EFF (embedded files) when a
|
|
368
|
+
// distinct effMethod is configured.
|
|
369
|
+
function makeEncryptForMethod(activeMethod) {
|
|
370
|
+
const typedEncrypt = {
|
|
371
|
+
V: version, R: revision, method: activeMethod
|
|
372
|
+
};
|
|
373
|
+
return function makeEnc(objNum, gen) {
|
|
374
|
+
if (activeMethod === 'AESV4') {
|
|
375
|
+
return function encryptOneGcm(plain) {
|
|
376
|
+
const ivProvider = () => rand(12);
|
|
377
|
+
return gcmMod.encryptObjectGcm(
|
|
378
|
+
fek, plain, ivProvider);
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (version === 4) {
|
|
382
|
+
const isEmbedded = activeMethod === effMethod
|
|
383
|
+
&& effMethod !== null
|
|
384
|
+
&& typeof handler.encryptEmbeddedFile === 'function';
|
|
385
|
+
return function encryptOneV4(plain) {
|
|
386
|
+
const iv = (activeMethod === 'AESV2')
|
|
387
|
+
? rand(16) : null;
|
|
388
|
+
return isEmbedded
|
|
389
|
+
? handler.encryptEmbeddedFile(
|
|
390
|
+
typedEncrypt, fek, objNum, gen, plain, iv)
|
|
391
|
+
: handler.encryptStream(
|
|
392
|
+
typedEncrypt, fek, objNum, gen, plain, iv);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return function encryptOneV5(plain) {
|
|
396
|
+
const iv = rand(16);
|
|
397
|
+
return handler.encryptStream(
|
|
398
|
+
typedEncrypt, fek, objNum, gen, plain, iv);
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const makeStmEnc = makeEncryptForMethod(method);
|
|
403
|
+
const makeEffEnc = effMethod
|
|
404
|
+
? makeEncryptForMethod(effMethod)
|
|
405
|
+
: null;
|
|
406
|
+
|
|
407
|
+
const inIndirects = opts.indirects || [];
|
|
408
|
+
const outIndirects = new Array(inIndirects.length);
|
|
409
|
+
for (let i = 0; i < inIndirects.length; i++) {
|
|
410
|
+
const it = inIndirects[i];
|
|
411
|
+
const encStm = makeStmEnc(it.num | 0, it.gen | 0);
|
|
412
|
+
const encEff = makeEffEnc
|
|
413
|
+
? makeEffEnc(it.num | 0, it.gen | 0)
|
|
414
|
+
: null;
|
|
415
|
+
outIndirects[i] = {
|
|
416
|
+
num: it.num,
|
|
417
|
+
gen: it.gen,
|
|
418
|
+
value: encryptObjectInPlace(it.value, encStm, encEff)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Step 3 — append /Encrypt indirect.
|
|
423
|
+
const encNum = nextObjNum(outIndirects);
|
|
424
|
+
const cfLength = (version === 4) ? 16 : 32;
|
|
425
|
+
outIndirects.push({
|
|
426
|
+
num: encNum,
|
|
427
|
+
gen: 0,
|
|
428
|
+
value: buildEncryptDict({
|
|
429
|
+
V: version, R: revision, keyBits, P,
|
|
430
|
+
O, U, OE, UE, Perms, encryptMetadata,
|
|
431
|
+
cfm: method, cfLength,
|
|
432
|
+
effCfm: effMethod || null,
|
|
433
|
+
effCfLength: effMethod ? cfLength : 0
|
|
434
|
+
})
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Step 4 — emit.
|
|
438
|
+
const bytes = writeEncryptedTrailer({
|
|
439
|
+
indirects: outIndirects,
|
|
440
|
+
root: opts.root,
|
|
441
|
+
info: opts.info,
|
|
442
|
+
id,
|
|
443
|
+
version: opts.version,
|
|
444
|
+
encryptRef: { num: encNum, gen: 0 }
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
bytes, encryptObjNum: encNum, fek, id,
|
|
449
|
+
O, U, OE, UE, Perms,
|
|
450
|
+
version, revision, method, effMethod
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// The base writeDocument does not know about /Encrypt in the
|
|
455
|
+
// trailer. We inline the same logic but inject the /Encrypt
|
|
456
|
+
// reference into the trailer dict. Implementation mirrors
|
|
457
|
+
// `pdfWriter.writeDocument` and is kept minimal — when the
|
|
458
|
+
// base writer learns /Encrypt natively this can be removed.
|
|
459
|
+
const te = new TextEncoder();
|
|
460
|
+
function concat(arrays) {
|
|
461
|
+
let n = 0;
|
|
462
|
+
for (const a of arrays) n += a.length;
|
|
463
|
+
const out = new Uint8Array(n);
|
|
464
|
+
let o = 0;
|
|
465
|
+
for (const a of arrays) { out.set(a, o); o += a.length; }
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
function writeEncryptedTrailer(opts) {
|
|
469
|
+
// Trick: emit the body via the base writer (header + objects +
|
|
470
|
+
// xref + trailer), then re-emit only the trailer to include
|
|
471
|
+
// /Encrypt. The base writer is stable and well-tested, so we
|
|
472
|
+
// instead rebuild a parallel path here. Keep it tight.
|
|
473
|
+
//
|
|
474
|
+
// Per the base writer we need: header → objects → xref →
|
|
475
|
+
// trailer (with /Encrypt added) → startxref → %%EOF.
|
|
476
|
+
// The writer module doesn't export its serializer. We must
|
|
477
|
+
// re-route through writeDocument with a placeholder, then
|
|
478
|
+
// patch the trailer.
|
|
479
|
+
//
|
|
480
|
+
// Simpler: re-implement here using the serializer we get
|
|
481
|
+
// from writerMod — but writerMod only exposes writeDocument
|
|
482
|
+
// / assembleIndirects. Strategy: call writeDocument, then
|
|
483
|
+
// patch the trailer dict to insert /Encrypt N 0 R.
|
|
484
|
+
|
|
485
|
+
const baseBytes = writeDocument({
|
|
486
|
+
indirects: opts.indirects,
|
|
487
|
+
root: opts.root,
|
|
488
|
+
info: opts.info,
|
|
489
|
+
id: opts.id,
|
|
490
|
+
version: opts.version
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Locate "trailer\n<< /Size ..." and inject " /Encrypt N 0 R"
|
|
494
|
+
// just before the closing " >>". Operates on bytes — the
|
|
495
|
+
// base writer guarantees ASCII for the trailer dict.
|
|
496
|
+
const trailerKey = te.encode('trailer\n<<');
|
|
497
|
+
let pos = -1;
|
|
498
|
+
for (let i = baseBytes.length - 256; i < baseBytes.length - trailerKey.length; i++) {
|
|
499
|
+
if (i < 0) continue;
|
|
500
|
+
let match = true;
|
|
501
|
+
for (let k = 0; k < trailerKey.length; k++) {
|
|
502
|
+
if (baseBytes[i + k] !== trailerKey[k]) { match = false; break; }
|
|
503
|
+
}
|
|
504
|
+
if (match) { pos = i; break;}
|
|
505
|
+
}
|
|
506
|
+
// Fallback — scan whole file (small enough).
|
|
507
|
+
if (pos < 0) {
|
|
508
|
+
outer: for (let i = 0; i < baseBytes.length - trailerKey.length; i++) {
|
|
509
|
+
for (let k = 0; k < trailerKey.length; k++) {
|
|
510
|
+
if (baseBytes[i + k] !== trailerKey[k]) continue outer;
|
|
511
|
+
}
|
|
512
|
+
pos = i; break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (pos < 0) {
|
|
516
|
+
throw new RenderError('pdf/crypto/enc-writer/no-trailer',
|
|
517
|
+
'base writer output did not contain a trailer');
|
|
518
|
+
}
|
|
519
|
+
// Find ' >>\n' after pos.
|
|
520
|
+
const close = te.encode(' >>\n');
|
|
521
|
+
let cposEnd = -1;
|
|
522
|
+
for (let i = pos; i < baseBytes.length - close.length; i++) {
|
|
523
|
+
let match = true;
|
|
524
|
+
for (let k = 0; k < close.length; k++) {
|
|
525
|
+
if (baseBytes[i + k] !== close[k]) { match = false; break; }
|
|
526
|
+
}
|
|
527
|
+
if (match) { cposEnd = i; break; }
|
|
528
|
+
}
|
|
529
|
+
if (cposEnd < 0) {
|
|
530
|
+
throw new RenderError('pdf/crypto/enc-writer/no-trailer-close',
|
|
531
|
+
'base writer output did not contain a closing trailer dict');
|
|
532
|
+
}
|
|
533
|
+
const inject = te.encode(
|
|
534
|
+
` /Encrypt ${opts.encryptRef.num} ${opts.encryptRef.gen | 0} R`);
|
|
535
|
+
|
|
536
|
+
// The trailer offset hasn't changed (objects + xref happen
|
|
537
|
+
// before trailer), but the startxref pointer encoded
|
|
538
|
+
// inside the trailer block also hasn't moved. We must,
|
|
539
|
+
// however, ensure that startxref still points to the same
|
|
540
|
+
// xref offset — which is true because we only inject
|
|
541
|
+
// bytes *after* the trailer offset in the file (within
|
|
542
|
+
// the trailer dict itself, which lives at the end).
|
|
543
|
+
//
|
|
544
|
+
// But xref offsets inside the table are NOT affected
|
|
545
|
+
// (they refer to object positions, all before the
|
|
546
|
+
// trailer). So we can splice safely.
|
|
547
|
+
const head = baseBytes.subarray(0, cposEnd);
|
|
548
|
+
const tail = baseBytes.subarray(cposEnd);
|
|
549
|
+
return concat([head, inject, tail]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return { writeEncryptedDocument };
|
|
553
|
+
}
|
|
554
|
+
};
|