pdfrb 0.8.6 → 0.8.7

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: 84368b352e856470e7d3f3374071df7724c21d5dba77596842ac5bacc419db59
4
- data.tar.gz: c0bfb8c0a8803aa74ecf43d1e3e35ff18b17bc457582705c409d8fae89b8f50f
3
+ metadata.gz: b0edf5439bfe359af4b900aeb846047b168d741271eb78a7f78315ca5b0bba55
4
+ data.tar.gz: 0b30e0edb04dcc2a4d26a493ba782379a8a864904eafc72f07c0c49ca4a25e57
5
5
  SHA512:
6
- metadata.gz: 4d6aec972d3b32bc66f70098ab1122472fef43c675d4eaba1b77cb4c3b74231a515f9ad877d648c8b28d3dc874b55475327b20ade2b2dcc85d70e3e15712fdbc
7
- data.tar.gz: 65fd26f6e44e1bcaa18471e86662f53e46ae4c2ac9a91a82fb182adbb10c7ef6616e6ab5505379dbc42c414a6684f848864648c486567a59e2f9aa695de67f57
6
+ metadata.gz: bd9802dee13e72edb052c80bd4fea095eade14c95b0fe8cb31277c0a9c36de9f9c202d99bbc3a485911dc5035d6253589b3f50358f0d2b789f4756c19d211e72
7
+ data.tar.gz: c44636603bba32db00de362646b378238fcb884008b085627c1e19d66e899472d9c1a2539ffffd3eb076a58788afb3abec2eaecfb9b44274960e025d31c7ed34
data/lib/pdfrb/cli.rb CHANGED
@@ -334,10 +334,16 @@ module Pdfrb
334
334
 
335
335
  # Set up the handler so Writer can encrypt per-object.
336
336
  handler = Pdfrb::Encryption::StandardSecurityHandler.new(
337
- Encrypt: { V: v, R: r, Length: length, P: perms, O: o_entry, U: u_entry },
338
- ID: [id0.b, id0.b]
337
+ {
338
+ Encrypt: { V: v, R: r, Length: length, P: perms,
339
+ O: o_entry, U: u_entry },
340
+ ID: [id0.b, id0.b],
341
+ }
339
342
  )
340
- handler.verify_user_password(user_pw)
343
+ unless handler.verify_user_password?(user_pw)
344
+ raise Pdfrb::EncryptionError, "failed to derive encryption key"
345
+ end
346
+
341
347
  doc.config["encryption.handler"] = handler
342
348
  doc.config["encryption.password"] = user_pw
343
349
  end
@@ -13,16 +13,17 @@ module Pdfrb
13
13
  autoload :TimestampHandler, "pdfrb/digital_signature/timestamp_handler"
14
14
  autoload :TimestampClient, "pdfrb/digital_signature/timestamp_client"
15
15
 
16
- HANDLERS = {
17
- "adbe.pkcs7.detached": CmsHandler,
18
- "adbe.pkcs7.sha1": CmsHandler,
19
- "adbe.revision": TimestampHandler,
20
- }.freeze
21
-
22
16
  module_function
23
17
 
18
+ # Resolve the handler class for +sub_filter+. Constant references
19
+ # live inside the case so the autoloads above defer loading the
20
+ # handlers (and their OpenSSL surface) until an actual
21
+ # sign/verify call needs them.
24
22
  def handler_for(sub_filter)
25
- HANDLERS[sub_filter.to_sym]
23
+ case sub_filter.to_sym
24
+ when :"adbe.pkcs7.detached", :"adbe.pkcs7.sha1" then CmsHandler
25
+ when :"adbe.revision" then TimestampHandler
26
+ end
26
27
  end
27
28
 
28
29
  def sign(document, cert:, key:, **)
@@ -30,7 +30,7 @@ module Pdfrb
30
30
 
31
31
  def initialize(document)
32
32
  @document = document
33
- @serializer = Pdfrb::Serializer.new
33
+ @serializer = Pdfrb::Writer.serializer_for(document)
34
34
  end
35
35
 
36
36
  # @param io [IO] target output.
@@ -70,7 +70,8 @@ module Pdfrb
70
70
 
71
71
  ordered.each do |obj|
72
72
  offsets[obj.oid] = temp.pos
73
- temp << @serializer.serialize_indirect(obj)
73
+ temp << @serializer.serialize_indirect(obj,
74
+ skip_string_encryption: encryption_dict?(obj))
74
75
  end
75
76
  first_page_end = temp.pos if first_page_objects.any?
76
77
 
@@ -79,7 +80,6 @@ module Pdfrb
79
80
  hint_compressed = ::Zlib::Deflate.deflate(hint_data)
80
81
  hint_offset = temp.pos
81
82
  hint_dict = @serializer.serialize(
82
- Type: :XRef,
83
83
  S: HintStream::DEFAULT_ITEM_BITS,
84
84
  Filter: :FlateDecode,
85
85
  Length: hint_compressed.bytesize
@@ -132,7 +132,8 @@ module Pdfrb
132
132
  buf << lin_padded
133
133
 
134
134
  layout.ordered_objects.each do |obj|
135
- buf << @serializer.serialize_indirect(obj)
135
+ buf << @serializer.serialize_indirect(obj,
136
+ skip_string_encryption: encryption_dict?(obj))
136
137
  end
137
138
 
138
139
  buf << "#{layout.hint_oid} 0 obj\n"
@@ -243,7 +244,16 @@ module Pdfrb
243
244
  end
244
245
  size = [@document.next_oid || 1, 2].max
245
246
 
246
- trailer_str = @serializer.serialize(Size: size, Root: root_ref)
247
+ trailer_hash = {}
248
+ (@document.trailer || {}).each do |k, v|
249
+ next if Pdfrb::Writer::TRAILER_OVERRIDDEN_KEYS.include?(k)
250
+
251
+ trailer_hash[k] = v
252
+ end
253
+ trailer_hash[:Size] = size
254
+ trailer_hash[:Root] = root_ref if root_ref
255
+
256
+ trailer_str = @serializer.serialize(trailer_hash)
247
257
  "trailer\n#{trailer_str}\nstartxref\n#{xref_offset}\n%%EOF\n"
248
258
  end
249
259
 
@@ -251,6 +261,11 @@ module Pdfrb
251
261
  "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"
252
262
  end
253
263
 
264
+ # The /Encrypt dictionary's own strings must stay in cleartext.
265
+ def encryption_dict?(obj)
266
+ (@document.trailer || {})[:Encrypt]&.oid == obj.oid
267
+ end
268
+
254
269
  def write_non_linearized(io)
255
270
  Pdfrb::Writer.write(@document, io)
256
271
  end
@@ -50,21 +50,28 @@ module Pdfrb
50
50
  end
51
51
 
52
52
  # Serialise an indirect object's body, including `oid gen obj`
53
- # header and (for streams) the stream payload.
54
- def serialize_indirect(obj)
53
+ # header and (for streams) the stream payload. When an encrypter
54
+ # is set, all strings in the object's dictionaries and the stream
55
+ # payload are encrypted with the per-object key (s7.6.1) — pass
56
+ # +skip_string_encryption:+ for the /Encrypt dictionary itself,
57
+ # whose entries must stay readable.
58
+ def serialize_indirect(obj, skip_string_encryption: false)
55
59
  raise ArgumentError, "not indirect: #{obj.inspect}" unless obj.indirect?
56
60
 
61
+ encrypt_strings = !skip_string_encryption && encrypter
57
62
  buffer = +""
58
63
  buffer << "#{obj.oid} #{obj.gen} obj\n"
59
64
  case obj
60
65
  when Pdfrb::Model::Cos::Stream
61
- dict, payload = emit_stream(obj)
66
+ dict_value = encrypt_strings ? encrypt_value_strings(obj.value, obj.oid, obj.gen) : obj.value
67
+ dict, payload = emit_stream(obj, dict_value)
62
68
  buffer << dict
63
69
  buffer << "\nstream\n"
64
70
  buffer << (encrypter ? encrypter.encrypt(payload, obj.oid, obj.gen) : payload)
65
71
  buffer << "\nendstream\n"
66
72
  else
67
- buffer << serialize(obj)
73
+ value = encrypt_strings ? encrypt_value_strings(obj.value, obj.oid, obj.gen) : obj.value
74
+ buffer << serialize(value)
68
75
  buffer << "\n"
69
76
  end
70
77
  buffer << "endobj\n"
@@ -73,12 +80,39 @@ module Pdfrb
73
80
 
74
81
  private
75
82
 
83
+ # Deep-transform +value+ so every literal string carries the
84
+ # per-object ciphertext (s7.6.1: strings AND streams encrypt).
85
+ def encrypt_value_strings(value, oid, gen)
86
+ case value
87
+ when ::Hash
88
+ value.each_with_object({}) do |(k, v), h|
89
+ h[k] = encrypt_value_strings(v, oid, gen)
90
+ end
91
+ when ::Array
92
+ value.map { |v| encrypt_value_strings(v, oid, gen) }
93
+ when Pdfrb::Model::PdfArray
94
+ Pdfrb::Model::PdfArray.new(value.map { |v| encrypt_value_strings(v, oid, gen) })
95
+ when Pdfrb::Model::Object
96
+ Pdfrb::Model::Object.new(encrypt_value_strings(value.value, oid, gen),
97
+ oid: value.oid, gen: value.gen)
98
+ when ::String
99
+ bytes = if value.encoding == Encoding::UTF_8
100
+ Pdfrb::Model::Cos::StringEncoding.encode_text(value)
101
+ else
102
+ value.dup.force_encoding(Encoding::BINARY)
103
+ end
104
+ encrypter.encrypt(bytes, oid, gen)
105
+ else
106
+ value
107
+ end
108
+ end
109
+
76
110
  # Build the dict body + payload for a stream object, applying
77
111
  # FlateDecode compression when enabled and the stream is large
78
112
  # enough to benefit. Returns [dict_bytes, payload_bytes].
79
- def emit_stream(stream_obj)
113
+ def emit_stream(stream_obj, dict_value = stream_obj.value)
80
114
  payload = stream_obj.stream
81
- hash = stream_obj.value.dup
115
+ hash = dict_value.dup
82
116
  existing_filter = hash[:Filter]
83
117
 
84
118
  if should_compress?(stream_obj, payload, existing_filter)
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.8.6"
4
+ VERSION = "0.8.7"
5
5
  end
data/lib/pdfrb/writer.rb CHANGED
@@ -17,24 +17,37 @@ module Pdfrb
17
17
  class Writer
18
18
  DEFAULT_VERSION = "1.4"
19
19
 
20
+ # Trailer keys each writer recomputes itself; existing values
21
+ # for these are never carried through from the parsed trailer.
22
+ TRAILER_OVERRIDDEN_KEYS = %i[Size Root Prev XRefStm].freeze
23
+
20
24
  attr_reader :document, :io, :serializer
21
25
 
22
26
  def initialize(document, io)
23
27
  @document = document
24
28
  @io = io
25
- @serializer = Serializer.new(
29
+ @serializer = Writer.serializer_for(document)
30
+ @xref_offsets = {}
31
+ end
32
+
33
+ # Build a Serializer configured for +document+: stream compression
34
+ # from config, and an encrypter derived from the trailer /Encrypt.
35
+ # Raises EncryptionError when the document is encrypted but the
36
+ # configured password does not open it — writing must never
37
+ # silently fall back to plaintext.
38
+ def self.serializer_for(document)
39
+ Serializer.new(
26
40
  compress_streams: document.config["writer.compress_streams"],
27
41
  compress_min_size: document.config["writer.compress_min_size"],
28
- **serializer_encrypter_opts
42
+ **serializer_encrypter_opts_for(document)
29
43
  )
30
- @xref_offsets = {}
31
44
  end
32
45
 
33
46
  # Build the encrypter option for the Serializer from the document's
34
47
  # /Encrypt dict. If the document isn't encrypted, returns empty hash.
35
48
  # The SecurityHandler's encrypt_data method becomes the Serializer's
36
49
  # encrypter, which encrypts string/stream payloads per-object.
37
- def serializer_encrypter_opts
50
+ def self.serializer_encrypter_opts_for(document)
38
51
  # Use a pre-configured handler if the document provides one
39
52
  # (e.g., set by the encryption CLI).
40
53
  handler = document.config["encryption.handler"]
@@ -50,15 +63,21 @@ module Pdfrb
50
63
  return {} unless encrypt_dict
51
64
 
52
65
  handler = Pdfrb::Encryption::StandardSecurityHandler.new(
53
- Encrypt: encrypt_dict.value,
54
- ID: trailer[:ID]
66
+ { Encrypt: encrypt_dict.value, ID: trailer[:ID] }
55
67
  )
56
68
  password = document.config["encryption.password"] || ""
57
- handler.verify_user_password(password)
69
+ unless handler.verify_user_password?(password)
70
+ raise Pdfrb::EncryptionError,
71
+ "cannot write encrypted document: password does not verify"
72
+ end
58
73
 
59
74
  { encrypter: handler }
60
- rescue StandardError
61
- {}
75
+ end
76
+
77
+ # The /Encrypt dictionary's own strings (U, O, UE, OE, Perms) must
78
+ # stay in cleartext — they carry the password hashes.
79
+ def encryption_dict?(obj)
80
+ (document.trailer || {})[:Encrypt]&.oid == obj.oid
62
81
  end
63
82
 
64
83
  def self.write(document, io)
@@ -84,7 +103,8 @@ module Pdfrb
84
103
  next if packed.key?(obj.oid)
85
104
 
86
105
  @xref_offsets[obj.oid] = @io.pos
87
- @io << @serializer.serialize_indirect(obj)
106
+ @io << @serializer.serialize_indirect(obj,
107
+ skip_string_encryption: encryption_dict?(obj))
88
108
  end
89
109
 
90
110
  xref_pos = if use_stream
@@ -118,7 +138,8 @@ module Pdfrb
118
138
  next unless obj.indirect?
119
139
 
120
140
  @xref_offsets[obj.oid] = @io.pos
121
- @io << @serializer.serialize_indirect(obj)
141
+ @io << @serializer.serialize_indirect(obj,
142
+ skip_string_encryption: encryption_dict?(obj))
122
143
  end
123
144
  xref_pos = write_xref(prev: previous_xref_offset)
124
145
  write_trailer(xref_pos, prev: previous_xref_offset)
@@ -293,7 +314,7 @@ module Pdfrb
293
314
  existing = document.trailer || {}
294
315
  trailer_hash = {}
295
316
  existing.each do |k, v|
296
- next if %i[Size Root Prev XRefStm].include?(k)
317
+ next if TRAILER_OVERRIDDEN_KEYS.include?(k)
297
318
 
298
319
  trailer_hash[k] = v
299
320
  end
data/lib/pdfrb.rb CHANGED
@@ -47,8 +47,6 @@ module Pdfrb
47
47
  autoload :Serializer, "pdfrb/serializer"
48
48
  autoload :Writer, "pdfrb/writer"
49
49
  autoload :Importer, "pdfrb/importer"
50
- autoload :Revision, "pdfrb/revision"
51
- autoload :Revisions, "pdfrb/revisions"
52
50
  autoload :XrefSection, "pdfrb/xref_section"
53
51
  autoload :Document, "pdfrb/document"
54
52
 
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.8.6
4
+ version: 0.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.