pdfrb 0.7.13 → 0.7.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebb3cb6d645dd2e4f01dbfdcfc49e51ed0857281fc996a38ddb29afb18aef4e7
4
- data.tar.gz: 9b359396f34164f661bebece73f26dd92fc9ea804c8edd802e53d80c6fccb334
3
+ metadata.gz: '0117148def4f7bc7280fa9727a6ce26fcafa9cb8f6e31cb32a480cb9548ac99e'
4
+ data.tar.gz: a7bf629aabbef013625f56e4c719f97a4fa6bc2823c158d4d7f37ea9c6ff8379
5
5
  SHA512:
6
- metadata.gz: a3eb6c87f3e7b10fdb310669943b3f61cf206c10f3ad61a998673b83f94dc6aef3082a22ef8f56632dad2e75a858b39c7a969a0590779f8ae3b7320c6c78ff90
7
- data.tar.gz: 9a1453e1556d9c968b3176aa207773e4083687441f7f8025941d82b4f34394a68fba1b3f7a386b4fb73743fb4770ca7c055276f28100b9686f40f6165eb69c76
6
+ metadata.gz: f7d17104eaf145c31e843f94e622dd181b965f18aa4e3aba4adb80bedbdb500855984c603ef2ce60fa4abec31d1e4a07841d96397a71452147a6bff19c122b94
7
+ data.tar.gz: e3513e197dff0e244f97adff17e332ff74b75509054449c32a779e8926bf1fbb91f9965b8a5757f78dbeb54f5e09741260b4139aef31f79cb0caaa25ca878b17
@@ -89,7 +89,7 @@ module Pdfrb
89
89
  trusted?: !trusted_certs.empty? && valid,
90
90
  error: valid ? nil : "signature verification failed",
91
91
  )
92
- rescue OpenSSL::PKCS7::PKCS7Error => e
92
+ rescue OpenSSL::PKCS7::PKCS7Error, ArgumentError => e
93
93
  VerificationResult.new(valid?: false,
94
94
  byte_range_ok?: true,
95
95
  cert_chain: [],
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+ require "securerandom"
5
+
6
+ module Pdfrb
7
+ class Document
8
+ # Encryption facade. Exposes a clean API for encrypting a
9
+ # document (builds the /Encrypt dict, wires the security handler
10
+ # into the writer config) and for checking encryption status.
11
+ #
12
+ # Usage:
13
+ # doc.encryption.encrypt!(
14
+ # user_password: "reader",
15
+ # owner_password: "owner",
16
+ # bits: 128,
17
+ # permissions: %i[print copy]
18
+ # )
19
+ # doc.write("encrypted.pdf")
20
+ class Encryption
21
+ PERMISSION_FLAGS = {
22
+ print: 1 << 2,
23
+ modify: 1 << 3,
24
+ copy: 1 << 4,
25
+ annotate: 1 << 5,
26
+ fill: 1 << 8,
27
+ extract: 1 << 9,
28
+ assemble: 1 << 10,
29
+ print_hq: 1 << 11,
30
+ }.freeze
31
+
32
+ attr_reader :document
33
+
34
+ def initialize(document)
35
+ @document = document
36
+ end
37
+
38
+ # Encrypt this document with a user + owner password.
39
+ #
40
+ # @param user_password [String] the password readers need to open.
41
+ # @param owner_password [String] the password that grants full
42
+ # permissions (defaults to user_password).
43
+ # @param bits [Integer] key length: 40, 128 (AES-128), or
44
+ # 256 (AES-256 R6).
45
+ # @param permissions [Array<Symbol>] granted permissions from
46
+ # PERMISSION_FLAGS keys; any not listed are denied.
47
+ # @return [Pdfrb::Model::Cos::Dictionary] the /Encrypt dict.
48
+ def encrypt!(user_password:, owner_password: nil, bits: 128,
49
+ permissions: PERMISSION_FLAGS.keys)
50
+ case bits
51
+ when 256 then encrypt_v5(user_password, owner_password || user_password, permissions)
52
+ when 128 then encrypt_rc4(user_password, owner_password || user_password,
53
+ permissions, v: 4, r: 4, length: 128)
54
+ when 40 then encrypt_rc4(user_password, owner_password || user_password,
55
+ permissions, v: 2, r: 3, length: 40)
56
+ else
57
+ raise Pdfrb::EncryptionError,
58
+ "unsupported key length: #{bits} (use 40, 128, or 256)"
59
+ end
60
+ end
61
+
62
+ # Whether the document has an /Encrypt dict in the trailer.
63
+ def encrypted?
64
+ trailer = document.trailer
65
+ !trailer.nil? && !trailer[:Encrypt].nil?
66
+ end
67
+
68
+ # Remove /Encrypt from the trailer (decrypt use case). Returns
69
+ # self for chaining; use encrypted? to check the result.
70
+ def decrypt!
71
+ trailer = document.trailer
72
+ if trailer && trailer[:Encrypt]
73
+ if trailer.is_a?(Pdfrb::Model::Cos::Dictionary)
74
+ trailer.value.delete(:Encrypt)
75
+ else
76
+ trailer.delete(:Encrypt)
77
+ end
78
+ document.config["encryption.handler"] = nil
79
+ document.config["encryption.password"] = nil
80
+ end
81
+ self
82
+ end
83
+
84
+ # Compute the /P integer: all bits set minus the denied ones.
85
+ def self.permission_bits(granted)
86
+ base = -1
87
+ PERMISSION_FLAGS.each do |name, bit|
88
+ base &= ~bit unless granted.include?(name)
89
+ end
90
+ base
91
+ end
92
+
93
+ private
94
+
95
+ def ensure_id!
96
+ id0 = Digest::MD5.hexdigest(Time.now.to_s + rand.to_s)[0, 16]
97
+ trailer = document.trailer
98
+ if trailer.is_a?(Pdfrb::Model::Cos::Dictionary)
99
+ trailer.value[:ID] = [id0.b, id0.b]
100
+ else
101
+ trailer[:ID] = [id0.b, id0.b]
102
+ end
103
+ id0.b
104
+ end
105
+
106
+ # RC4-based encryption for V2/V4 (40-bit or 128-bit).
107
+ def encrypt_rc4(user_pw, owner_pw, permissions, v:, r:, length:)
108
+ p_bits = self.class.permission_bits(permissions)
109
+ id0 = ensure_id!
110
+
111
+ o_entry = compute_o_entry(user_pw, owner_pw, r, length)
112
+ key = Pdfrb::Encryption::PasswordVerification.derive_key_rc4(
113
+ password: user_pw, o_entry: o_entry.b, p_flags: p_bits,
114
+ id0: id0, revision: r, key_length_bits: length,
115
+ encrypt_metadata: true
116
+ )
117
+ u_entry =
118
+ if r == 2
119
+ Pdfrb::Encryption::PasswordVerification.build_u_r2(key)
120
+ else
121
+ Pdfrb::Encryption::PasswordVerification.build_u_r3plus(
122
+ file_key: key, id0: id0, revision: r
123
+ )
124
+ end
125
+
126
+ register({ Filter: :Standard, V: v, R: r, Length: length,
127
+ P: p_bits, O: o_entry, U: u_entry },
128
+ user_password: user_pw)
129
+ end
130
+
131
+ # AES-256 R6 encryption for V5.
132
+ def encrypt_v5(user_pw, owner_pw, permissions)
133
+ p_bits = self.class.permission_bits(permissions)
134
+ id0 = ensure_id!
135
+
136
+ handler = Pdfrb::Encryption::StandardSecurityHandler.for_v5(
137
+ user_password: user_pw, owner_password: owner_pw,
138
+ permissions: p_bits, id: id0
139
+ )
140
+ dict = handler.encrypt_dict
141
+ register({ Filter: :Standard, V: dict[:V], R: dict[:R],
142
+ Length: dict[:Length], P: p_bits,
143
+ O: dict[:O], U: dict[:U], UE: dict[:UE],
144
+ OE: dict[:OE], Perms: dict[:Perms] },
145
+ user_password: user_pw,
146
+ prebuilt_handler: handler)
147
+ end
148
+
149
+ # Compute the /O entry per Algorithm 3 (owner password hash).
150
+ def compute_o_entry(user_pw, owner_pw, revision, length_bits)
151
+ require "digest/md5"
152
+ pv = Pdfrb::Encryption::PasswordVerification
153
+ padded = pv.pad_password(owner_pw)
154
+ digest = Digest::MD5.digest(padded)
155
+ if revision >= 3
156
+ 50.times do
157
+ digest = if length_bits >= 128
158
+ Digest::MD5.digest(digest[0, length_bits / 8])
159
+ else
160
+ Digest::MD5.digest(digest[0, 5])
161
+ end
162
+ end
163
+ end
164
+ rc4 = Pdfrb::Encryption::RC4Impl.new(digest[0, length_bits / 8])
165
+ o = rc4.process(pv.pad_password(user_pw))
166
+ if revision >= 3
167
+ 19.downto(1) do |i|
168
+ key = digest[0, length_bits / 8].bytes.map { |b| b ^ i }.pack("C*")
169
+ o = Pdfrb::Encryption::RC4Impl.new(key).process(o)
170
+ end
171
+ end
172
+ o
173
+ end
174
+
175
+ # Register the encrypt dict in the trailer + wire the handler.
176
+ def register(encrypt_hash, user_password:, prebuilt_handler: nil)
177
+ dict = document.add(encrypt_hash,
178
+ type: Pdfrb::Model::Type::EncryptionStandard)
179
+ ref = Pdfrb::Model::Reference.new(dict.oid, dict.gen)
180
+ trailer = document.trailer
181
+ if trailer.is_a?(Pdfrb::Model::Cos::Dictionary)
182
+ trailer.value[:Encrypt] = ref
183
+ else
184
+ trailer[:Encrypt] = ref
185
+ end
186
+
187
+ handler = prebuilt_handler || begin
188
+ h = Pdfrb::Encryption::StandardSecurityHandler.new(
189
+ { Encrypt: encrypt_hash,
190
+ ID: if trailer.is_a?(Pdfrb::Model::Cos::Dictionary)
191
+ trailer.value[:ID]
192
+ else
193
+ trailer[:ID]
194
+ end }
195
+ )
196
+ h.verify_user_password?(user_password)
197
+ h
198
+ end
199
+ document.config["encryption.handler"] = handler
200
+ document.config["encryption.password"] = user_password
201
+ dict
202
+ end
203
+ end
204
+ end
205
+ end
@@ -31,6 +31,7 @@ module Pdfrb
31
31
  autoload :Info, "pdfrb/document/info"
32
32
  autoload :PageLabels, "pdfrb/document/page_labels"
33
33
  autoload :Stamps, "pdfrb/document/stamps"
34
+ autoload :Encryption, "pdfrb/document/encryption"
34
35
 
35
36
  def initialize(io: nil, config: {})
36
37
  Pdfrb::Model::Type.eager_load!
@@ -163,6 +164,8 @@ module Pdfrb
163
164
 
164
165
  def stamps; @stamps ||= Document::Stamps.new(self); end
165
166
 
167
+ def encryption; @encryption ||= Document::Encryption.new(self); end
168
+
166
169
  def info; @info ||= Document::Info.new(self); end
167
170
 
168
171
  def display; @display ||= Document::Display.new(self); end
@@ -219,7 +219,11 @@ module Pdfrb
219
219
  hash = md5.digest
220
220
 
221
221
  rc4 = RC4Impl.new(key)
222
- 20.times { |i| hash = rc4.process(hash, key ^ i) }
222
+ 19.downto(1) do |i|
223
+ xored_key = key.bytes.map { |b| b ^ i }.pack("C*")
224
+ rc4 = RC4Impl.new(xored_key)
225
+ hash = rc4.process(hash)
226
+ end
223
227
 
224
228
  hash[0, 16]
225
229
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ module Model
5
+ module Type
6
+ # Thumbnail image stream (ISO 32000-2 §12.3.4). A small image
7
+ # XObject rendered at reduced resolution for use in viewer
8
+ # navigation panels. Attached to a page via the /Thumb key.
9
+ class Thumbnail < Pdfrb::Model::Cos::Stream
10
+ arlington_object "Thumbnail"
11
+
12
+ # /Width — required.
13
+ def width
14
+ value[:Width]
15
+ end
16
+
17
+ # /Height — required.
18
+ def height
19
+ value[:Height]
20
+ end
21
+
22
+ # /ColorSpace — required unless /ImageMask true.
23
+ def color_space
24
+ value[:ColorSpace]
25
+ end
26
+
27
+ # /BitsPerComponent — required.
28
+ def bits_per_component
29
+ value[:BitsPerComponent]
30
+ end
31
+
32
+ # /Filter — optional; a named filter or array of filters.
33
+ def filter
34
+ value[:Filter]
35
+ end
36
+
37
+ def dimensions
38
+ [width, height]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ module Model
5
+ module Type
6
+ # Timespan (ISO 32000-2 §12.7.11, PDF 1.5+). A duration with
7
+ # a unit, used in RichMedia timing, 3D animation, and
8
+ # JavaScript-embedded timelines.
9
+ class Timespan < Pdfrb::Model::Cos::Dictionary
10
+ arlington_object "Timespan"
11
+
12
+ # /Type — optional, fixed "Timespan".
13
+ def type
14
+ value[:Type]&.to_sym
15
+ end
16
+
17
+ # /S — required, fixed "S" (seconds).
18
+ def subtype
19
+ value[:S]&.to_sym
20
+ end
21
+
22
+ # /V — required, the numeric duration in seconds.
23
+ def duration
24
+ value[:V]
25
+ end
26
+
27
+ # Convenience: duration in milliseconds.
28
+ def milliseconds
29
+ v = duration
30
+ v ? v.to_f * 1000 : nil
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ module Model
5
+ module Type
6
+ # UserProperty (ISO 32000-2 §14.7.5). A single property in the
7
+ # /P array of a user property list attached to structure
8
+ # elements for custom metadata.
9
+ class UserProperty < Pdfrb::Model::Cos::Dictionary
10
+ arlington_object "UserProperty"
11
+
12
+ # /N — required, the property name.
13
+ def name
14
+ value[:N]
15
+ end
16
+
17
+ # /V — required, the property value (any COS type).
18
+ def property_value
19
+ value[:V]
20
+ end
21
+
22
+ # /F — optional, a text string with the intended format of
23
+ # the value.
24
+ def format
25
+ value[:F]
26
+ end
27
+
28
+ # /H — optional, whether the property is hidden from the
29
+ # user (default false).
30
+ def hidden?
31
+ value[:H] == true
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ module Model
5
+ module Type
6
+ # Viewport (ISO 32000-2 §12.7.11, PDF 1.6+). Defines a
7
+ # rectangular region of a page intended for separate display,
8
+ # used for magnifier rectangles, measurement regions, and
9
+ # zoom targets.
10
+ class Viewport < Pdfrb::Model::Cos::Dictionary
11
+ arlington_object "Viewport"
12
+
13
+ # /Type — optional, fixed "Viewport".
14
+ def type
15
+ value[:Type]&.to_sym
16
+ end
17
+
18
+ # /BBox — required, the viewport rectangle.
19
+ def bbox
20
+ value[:BBox]
21
+ end
22
+
23
+ # /Name — optional, human-readable viewport name.
24
+ def name
25
+ value[:Name]
26
+ end
27
+
28
+ # /Measure — optional indirect Measure dict (RL for rulers,
29
+ # GEO for geospatial since PDF 2.0).
30
+ def measure(document = nil)
31
+ ref = value[:Measure]
32
+ return nil unless ref && document
33
+
34
+ ref.is_a?(Pdfrb::Model::Reference) ? document.object(ref) : ref
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ module Model
5
+ module Type
6
+ # VRI (Validation Related Information) dictionary per ETSI EN
7
+ # 319 142-1 (PAdES) §B.5. Lives in /DSS /VRI map, keyed by the
8
+ # hex-encoded SHA-1 of the signature it validates. Holds the
9
+ # certificates, CRLs, and OCSP responses relevant to one
10
+ # signature.
11
+ class Vri < Pdfrb::Model::Cos::Dictionary
12
+ arlington_object "VRI"
13
+
14
+ # /Type — optional, fixed "VRI".
15
+ def type
16
+ value[:Type]&.to_sym
17
+ end
18
+
19
+ # /Cert — array of indirect streams, each a DER-encoded
20
+ # X.509 certificate relevant to the signature.
21
+ def certificates(document = nil)
22
+ refs = value[:Cert]
23
+ return [] unless refs
24
+
25
+ resolve_refs(refs, document)
26
+ end
27
+
28
+ # /CRL — array of indirect streams, each a DER-encoded
29
+ # certificate revocation list.
30
+ def crls(document = nil)
31
+ refs = value[:CRL]
32
+ return [] unless refs
33
+
34
+ resolve_refs(refs, document)
35
+ end
36
+
37
+ # /OCSP — array of indirect streams, each a DER-encoded
38
+ # OCSP response.
39
+ def ocsp_responses(document = nil)
40
+ refs = value[:OCSP]
41
+ return [] unless refs
42
+
43
+ resolve_refs(refs, document)
44
+ end
45
+
46
+ # /TU — optional string-text with a human-readable timestamp.
47
+ def timestamp_text
48
+ value[:TU]
49
+ end
50
+
51
+ # /TS — optional indirect stream with a timestamp token.
52
+ def timestamp_token(document = nil)
53
+ ref = value[:TS]
54
+ return nil unless ref && document
55
+
56
+ ref.is_a?(Pdfrb::Model::Reference) ? document.object(ref) : ref
57
+ end
58
+
59
+ private
60
+
61
+ def resolve_refs(refs, document)
62
+ return [] unless document
63
+
64
+ arr = refs.is_a?(Pdfrb::Model::PdfArray) ? refs.value : refs
65
+ arr = [arr] unless arr.is_a?(::Array)
66
+ arr.filter_map do |r|
67
+ next r unless r.is_a?(Pdfrb::Model::Reference)
68
+
69
+ document.object(r)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -356,6 +356,21 @@ module Pdfrb
356
356
 
357
357
  # Page-piece dictionary (s14.5).
358
358
  autoload :PagePieceInfo, "pdfrb/model/type/page_piece_info"
359
+
360
+ # PAdES validation-related information (ETSI EN 319 142-1).
361
+ autoload :Vri, "pdfrb/model/type/vri"
362
+
363
+ # Page thumbnails (s12.3.4).
364
+ autoload :Thumbnail, "pdfrb/model/type/thumbnail"
365
+
366
+ # Timespan (s12.7.11, PDF 1.5+).
367
+ autoload :Timespan, "pdfrb/model/type/timespan"
368
+
369
+ # Viewport (s12.7.11, PDF 1.6+).
370
+ autoload :Viewport, "pdfrb/model/type/viewport"
371
+
372
+ # User property (s14.7.5).
373
+ autoload :UserProperty, "pdfrb/model/type/user_property"
359
374
  end
360
375
  end
361
376
  end
data/lib/pdfrb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.7.13"
4
+ VERSION = "0.7.15"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pdfrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.13
4
+ version: 0.7.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -817,6 +817,7 @@ files:
817
817
  - lib/pdfrb/document/colors.rb
818
818
  - lib/pdfrb/document/destinations.rb
819
819
  - lib/pdfrb/document/display.rb
820
+ - lib/pdfrb/document/encryption.rb
820
821
  - lib/pdfrb/document/files.rb
821
822
  - lib/pdfrb/document/fonts.rb
822
823
  - lib/pdfrb/document/form.rb
@@ -1149,14 +1150,19 @@ files:
1149
1150
  - lib/pdfrb/model/type/three_d_stream.rb
1150
1151
  - lib/pdfrb/model/type/three_d_units.rb
1151
1152
  - lib/pdfrb/model/type/three_d_view.rb
1153
+ - lib/pdfrb/model/type/thumbnail.rb
1154
+ - lib/pdfrb/model/type/timespan.rb
1152
1155
  - lib/pdfrb/model/type/to_unicode_cmap_stream.rb
1153
1156
  - lib/pdfrb/model/type/transition.rb
1154
1157
  - lib/pdfrb/model/type/underline_annotation.rb
1155
1158
  - lib/pdfrb/model/type/ur_transform_parameters.rb
1156
1159
  - lib/pdfrb/model/type/uri_dict.rb
1157
1160
  - lib/pdfrb/model/type/url_alias.rb
1161
+ - lib/pdfrb/model/type/user_property.rb
1158
1162
  - lib/pdfrb/model/type/variable_text_field.rb
1159
1163
  - lib/pdfrb/model/type/viewer_preferences.rb
1164
+ - lib/pdfrb/model/type/viewport.rb
1165
+ - lib/pdfrb/model/type/vri.rb
1160
1166
  - lib/pdfrb/model/type/watermark_annotation.rb
1161
1167
  - lib/pdfrb/model/type/widget_annotation.rb
1162
1168
  - lib/pdfrb/model/type/widget_appearance.rb