pdfrb 0.8.7 → 0.8.8

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: b0edf5439bfe359af4b900aeb846047b168d741271eb78a7f78315ca5b0bba55
4
- data.tar.gz: 0b30e0edb04dcc2a4d26a493ba782379a8a864904eafc72f07c0c49ca4a25e57
3
+ metadata.gz: af5b6aea283bcc7325c584008268aaedc228f78882cb5408b615c55576cb8154
4
+ data.tar.gz: 29c3a794f7829144e4fa2275ba4a7b4124087abd7279f076689b06d14d5ac4c3
5
5
  SHA512:
6
- metadata.gz: bd9802dee13e72edb052c80bd4fea095eade14c95b0fe8cb31277c0a9c36de9f9c202d99bbc3a485911dc5035d6253589b3f50358f0d2b789f4756c19d211e72
7
- data.tar.gz: c44636603bba32db00de362646b378238fcb884008b085627c1e19d66e899472d9c1a2539ffffd3eb076a58788afb3abec2eaecfb9b44274960e025d31c7ed34
6
+ metadata.gz: 6491c0a75e0ef6eb6fa849fdf25de0d546c2055b099e3f533595a073e370a30c37693604b31620e5d3707237e1960f18f6ba7ee05555698c21b081e6300cf493
7
+ data.tar.gz: 569d900685cc748edba60dfcdc426987343025c4ccae008a715b188ce41ce9d56abf29901debb39e1292d856ba34951394b85a57efb297ac9b5af0b67f706eb6
data/lib/pdfrb/cli.rb CHANGED
@@ -325,9 +325,10 @@ module Pdfrb
325
325
  )
326
326
  end
327
327
 
328
+ cf_entries = Pdfrb::Encryption.v4_crypt_filters(v)
328
329
  encrypt_dict = doc.add(
329
330
  { Filter: :Standard, V: v, R: r, Length: length, P: perms,
330
- O: o_entry, U: u_entry },
331
+ O: o_entry, U: u_entry, **cf_entries },
331
332
  type: Pdfrb::Model::Cos::Dictionary
332
333
  )
333
334
  doc.trailer[:Encrypt] = encrypt_dict.ref
@@ -336,7 +337,7 @@ module Pdfrb
336
337
  handler = Pdfrb::Encryption::StandardSecurityHandler.new(
337
338
  {
338
339
  Encrypt: { V: v, R: r, Length: length, P: perms,
339
- O: o_entry, U: u_entry },
340
+ O: o_entry, U: u_entry, **cf_entries },
340
341
  ID: [id0.b, id0.b],
341
342
  }
342
343
  )
@@ -65,6 +65,30 @@ module Pdfrb
65
65
  !trailer.nil? && !trailer[:Encrypt].nil?
66
66
  end
67
67
 
68
+ # Handler for reading an encrypted document: memoized, built
69
+ # from the trailer /Encrypt, verifying the configured password
70
+ # (config "encryption.password"). Returns nil for unencrypted
71
+ # documents. Raises EncryptionError when the password does not
72
+ # verify — the reader must never hand back ciphertext as
73
+ # plaintext.
74
+ def reader_handler
75
+ return nil unless encrypted?
76
+
77
+ @reader_handler ||= begin
78
+ trailer = document.trailer
79
+ encrypt_dict = document.resolve(trailer[:Encrypt])
80
+ handler = Pdfrb::Encryption::StandardSecurityHandler.new(
81
+ { Encrypt: encrypt_dict.value, ID: trailer[:ID] }
82
+ )
83
+ password = document.config["encryption.password"] || ""
84
+ unless handler.verify_user_password?(password)
85
+ raise Pdfrb::EncryptionError,
86
+ "document is encrypted and the password does not verify"
87
+ end
88
+ handler
89
+ end
90
+ end
91
+
68
92
  # Remove /Encrypt from the trailer (decrypt use case). Returns
69
93
  # self for chaining; use encrypted? to check the result.
70
94
  def decrypt!
@@ -78,6 +102,7 @@ module Pdfrb
78
102
  document.config["encryption.handler"] = nil
79
103
  document.config["encryption.password"] = nil
80
104
  end
105
+ @reader_handler = nil
81
106
  self
82
107
  end
83
108
 
@@ -123,8 +148,9 @@ module Pdfrb
123
148
  )
124
149
  end
125
150
 
151
+ cf_entries = Pdfrb::Encryption.v4_crypt_filters(v)
126
152
  register({ Filter: :Standard, V: v, R: r, Length: length,
127
- P: p_bits, O: o_entry, U: u_entry },
153
+ P: p_bits, O: o_entry, U: u_entry, **cf_entries },
128
154
  user_password: user_pw)
129
155
  end
130
156
 
@@ -193,7 +219,10 @@ module Pdfrb
193
219
  trailer[:ID]
194
220
  end }
195
221
  )
196
- h.verify_user_password?(user_password)
222
+ unless h.verify_user_password?(user_password)
223
+ raise Pdfrb::EncryptionError,
224
+ "derived key does not verify against the /U entry"
225
+ end
197
226
  h
198
227
  end
199
228
  document.config["encryption.handler"] = handler
@@ -7,6 +7,10 @@ module Pdfrb
7
7
 
8
8
  def encrypt(data, **_opts); data; end
9
9
  def decrypt(data, **_opts); data; end
10
+ def encrypt_string(data, *_); data; end
11
+ def encrypt_stream(data, *_); data; end
12
+ def decrypt_string(data, *_); data; end
13
+ def decrypt_stream(data, *_); data; end
10
14
  def key_length; 0; end
11
15
  end
12
16
  end
@@ -99,22 +99,26 @@ module Pdfrb
99
99
  rc4.process(pack_bytes(PADDING_BYTES))
100
100
  end
101
101
 
102
- # Algorithm 5 (R>=3): build the /U entry.
103
- def build_u_r3plus(file_key:, id0:, revision:)
102
+ # Algorithm 5 (R>=3): the /U entry. The 19 additional RC4
103
+ # rounds use the file key XORed with the round index, for
104
+ # i = 1..19 in ascending order.
105
+ def user_password_hash_r3plus(file_key:, id0:)
104
106
  digest = Digest::MD5.new
105
107
  digest.update(pack_bytes(PADDING_BYTES))
106
108
  digest.update(id0.b)
107
109
  hash = digest.digest
108
- rc4 = RC4.new(file_key)
109
- encrypted = rc4.process(hash)
110
- # 19 more rounds with key XORed by round index.
111
- 19.times do |i|
110
+ hash = RC4.new(file_key).process(hash)
111
+ (1..19).each do |i|
112
112
  key = file_key.bytes.map { |b| (b ^ i).chr }.join
113
- encrypted = RC4.new(key).process(encrypted)
113
+ hash = RC4.new(key).process(hash)
114
114
  end
115
- # Pad / truncate to 32 bytes.
116
- encrypted = encrypted + ("\x00" * (32 - encrypted.bytesize))
117
- encrypted.byteslice(0, 32)
115
+ hash
116
+ end
117
+
118
+ def build_u_r3plus(file_key:, id0:, revision:)
119
+ hash = user_password_hash_r3plus(file_key: file_key, id0: id0)
120
+ # Pad to 32 bytes.
121
+ hash + ("\x00" * (32 - hash.bytesize))
118
122
  end
119
123
 
120
124
  # Algorithm 6: verify a user password. Returns true if it
@@ -85,6 +85,22 @@ module Pdfrb
85
85
 
86
86
  # Per-object encryption: AES-256-CBC with IV prefix. The
87
87
  # per-object key is SHA-256(file_key ‖ oid ‖ gen).
88
+ def encrypt_string(data, oid, gen)
89
+ encrypt_data(data, oid, gen)
90
+ end
91
+
92
+ def encrypt_stream(data, oid, gen)
93
+ encrypt_data(data, oid, gen)
94
+ end
95
+
96
+ def decrypt_string(data, oid, gen)
97
+ decrypt_data(data, oid, gen)
98
+ end
99
+
100
+ def decrypt_stream(data, oid, gen)
101
+ decrypt_data(data, oid, gen)
102
+ end
103
+
88
104
  def encrypt_data(data, oid, gen)
89
105
  return data unless @file_key
90
106
 
@@ -173,11 +173,77 @@ module Pdfrb
173
173
  end
174
174
  end
175
175
 
176
+ # Direction-aware entry points (s7.6.3.2 Table 21): streams
177
+ # use the StmF crypt filter, strings use StrF. V5 is always
178
+ # AES-256; V2/V3 are always RC4; V4 resolves the cipher from
179
+ # the CF dictionary's CFM (defaulting to Identity, per spec).
180
+ def encrypt_string(data, oid, gen)
181
+ encrypt_with(str_crypt_method, data, oid, gen)
182
+ end
183
+
184
+ def encrypt_stream(data, oid, gen)
185
+ encrypt_with(stm_crypt_method, data, oid, gen)
186
+ end
187
+
188
+ def decrypt_string(data, oid, gen)
189
+ decrypt_with(str_crypt_method, data, oid, gen)
190
+ end
191
+
192
+ def decrypt_stream(data, oid, gen)
193
+ decrypt_with(stm_crypt_method, data, oid, gen)
194
+ end
195
+
176
196
  # Serializer-compatible alias for encrypt_data. The Serializer
177
197
  # calls encrypter.encrypt(payload, oid, gen) on string/stream
178
198
  # payloads during serialization.
179
199
  alias encrypt encrypt_data
180
200
 
201
+ def stm_crypt_method
202
+ crypt_method_for(@encrypt[:StmF])
203
+ end
204
+
205
+ def str_crypt_method
206
+ crypt_method_for(@encrypt[:StrF])
207
+ end
208
+
209
+ # @return [:aes256, :aes, :rc4, :identity]
210
+ def crypt_method_for(filter_name)
211
+ return :aes256 if @version >= 5
212
+ return :rc4 if @version <= 3
213
+
214
+ # V4: resolve CFM from the CF dictionary; absent filters
215
+ # default to Identity (s7.6.3.2 Table 21).
216
+ return :identity if filter_name.nil? || filter_name == :Identity
217
+
218
+ cf = @encrypt[:CF]
219
+ cfm = cf && cf[filter_name] ? cf[filter_name][:CFM] : nil
220
+ case cfm
221
+ when :AESV2 then :aes
222
+ when :V2 then :rc4
223
+ else :identity
224
+ end
225
+ end
226
+
227
+ def encrypt_with(method, data, oid, gen)
228
+ return data if method == :identity || @key.nil?
229
+
230
+ obj_key = compute_object_key(oid, gen)
231
+ case method
232
+ when :aes256, :aes then encrypt_aes_cbc(data, obj_key)
233
+ else encrypt_rc4(data, obj_key)
234
+ end
235
+ end
236
+
237
+ def decrypt_with(method, data, oid, gen)
238
+ return data if method == :identity || @key.nil?
239
+
240
+ obj_key = compute_object_key(oid, gen)
241
+ case method
242
+ when :aes256, :aes then decrypt_aes_cbc(data, obj_key)
243
+ else decrypt_rc4(data, obj_key)
244
+ end
245
+ end
246
+
181
247
  def decrypt_data(data, oid, gen)
182
248
  return data unless @key
183
249
 
@@ -194,7 +260,9 @@ module Pdfrb
194
260
 
195
261
  def pad_password(password)
196
262
  pw = password.to_s.encode(Encoding::BINARY)[0, 32]
197
- pw + PAD[pw.bytesize, 32 - pw.bytesize]
263
+ # Algorithm 3.2: append the FIRST (32 - n) bytes of the pad
264
+ # string — not a slice offset by the password length.
265
+ pw + PAD[0, 32 - pw.bytesize]
198
266
  end
199
267
 
200
268
  def id_bytes
@@ -205,10 +273,14 @@ module Pdfrb
205
273
  end
206
274
 
207
275
  def verify_user_password_hash?(key)
208
- return true if @revision < 3
209
-
210
- hash = compute_user_password_hash(key)
211
- stored = @encrypt[:U] || ""
276
+ stored = @encrypt[:U] || "".b
277
+ hash = if @revision < 3
278
+ # Algorithm 3.4/3.6 (R2): U = RC4(file_key, PAD).
279
+ RC4Impl.new(key).process(PAD)
280
+ else
281
+ Pdfrb::Encryption::PasswordVerification
282
+ .user_password_hash_r3plus(file_key: key, id0: id_bytes)
283
+ end
212
284
  hash[0, 16] == stored[0, 16]
213
285
  end
214
286
 
@@ -236,11 +308,15 @@ module Pdfrb
236
308
  RC4Impl.new(key).process(data)
237
309
  end
238
310
 
311
+ def aes_cipher_name(key)
312
+ "aes-#{key.bytesize * 8}-cbc"
313
+ end
314
+
239
315
  def encrypt_aes_cbc(data, key)
240
316
  iv = OpenSSL::Random.random_bytes(16)
241
- cipher = OpenSSL::Cipher.new("aes-128-cbc")
317
+ cipher = OpenSSL::Cipher.new(aes_cipher_name(key))
242
318
  cipher.encrypt
243
- cipher.key = key[0, 16]
319
+ cipher.key = key
244
320
  cipher.iv = iv
245
321
  encrypted = cipher.update(data) + cipher.final
246
322
  iv + encrypted
@@ -251,9 +327,9 @@ module Pdfrb
251
327
 
252
328
  iv = data[0, 16]
253
329
  ciphertext = data[16..]
254
- decipher = OpenSSL::Cipher.new("aes-128-cbc")
330
+ decipher = OpenSSL::Cipher.new(aes_cipher_name(key))
255
331
  decipher.decrypt
256
- decipher.key = key[0, 16]
332
+ decipher.key = key
257
333
  decipher.iv = iv
258
334
  decipher.update(ciphertext) + decipher.final
259
335
  rescue OpenSSL::Cipher::CipherError
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ module Encryption
5
+ # The COS value-tree string walk, shared by both directions of
6
+ # the encryption seam: the Serializer encrypts every literal
7
+ # string an indirect object carries (building transformed
8
+ # copies), and the ObjectReader decrypts them back (mutating the
9
+ # freshly-parsed containers in place). One walk, two adapters —
10
+ # keeps the string set and traversal rules from drifting.
11
+ module ValueStrings
12
+ module_function
13
+
14
+ # @return [Object] a transformed copy of +value+ with every
15
+ # literal string replaced by the block's result.
16
+ def encrypt(value, oid, gen, cipher)
17
+ case value
18
+ when ::Hash
19
+ value.each_with_object({}) { |(k, v), h| h[k] = encrypt(v, oid, gen, cipher) }
20
+ when ::Array
21
+ value.map { |v| encrypt(v, oid, gen, cipher) }
22
+ when Pdfrb::Model::PdfArray
23
+ Pdfrb::Model::PdfArray.new(value.map { |v| encrypt(v, oid, gen, cipher) })
24
+ when Pdfrb::Model::Object
25
+ Pdfrb::Model::Object.new(encrypt(value.value, oid, gen, cipher),
26
+ oid: value.oid, gen: value.gen)
27
+ when ::String
28
+ bytes = if value.encoding == Encoding::UTF_8
29
+ Pdfrb::Model::Cos::StringEncoding.encode_text(value)
30
+ else
31
+ value.dup.force_encoding(Encoding::BINARY)
32
+ end
33
+ cipher.encrypt_string(bytes, oid, gen)
34
+ else
35
+ value
36
+ end
37
+ end
38
+
39
+ # Replaces every literal string inside the freshly-parsed
40
+ # +value+ (a Hash/Array container tree) with its decrypted
41
+ # form, in place. Returns +value+.
42
+ def decrypt!(value, oid, gen, cipher)
43
+ case value
44
+ when ::Hash
45
+ value.each { |k, v| value[k] = decrypt!(v, oid, gen, cipher) }
46
+ when ::Array
47
+ value.map! { |v| decrypt!(v, oid, gen, cipher) }
48
+ when Pdfrb::Model::PdfArray
49
+ value.value.map! { |v| decrypt!(v, oid, gen, cipher) }
50
+ value
51
+ when ::String
52
+ cipher.decrypt_string(value, oid, gen)
53
+ else
54
+ value
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -17,6 +17,7 @@ module Pdfrb
17
17
  autoload :AES, "pdfrb/encryption/aes"
18
18
  autoload :PasswordVerification, "pdfrb/encryption/password_verification"
19
19
  autoload :Identity, "pdfrb/encryption/identity"
20
+ autoload :ValueStrings, "pdfrb/encryption/value_strings"
20
21
  autoload :PublicKeySecurityHandler, "pdfrb/encryption/public_key_security_handler"
21
22
  autoload :V5Writer, "pdfrb/encryption/v5_writer"
22
23
 
@@ -32,6 +33,29 @@ module Pdfrb
32
33
  def handler_for_document(document, **opts)
33
34
  SecurityHandler.for(document, decryption_opts: opts)
34
35
  end
36
+
37
+ # Crypt-filter entries a V4 /Encrypt dict must carry when the
38
+ # cipher is AES-128: absent StmF/StrF default to Identity
39
+ # (s7.6.3.2 Table 21), leaving the ciphertext unreadable.
40
+ V4_AESV2_FILTERS = {
41
+ CF: { StdCF: { CFM: :AESV2, Length: 16 } },
42
+ StmF: :StdCF,
43
+ StrF: :StdCF,
44
+ }.freeze
45
+
46
+ def v4_crypt_filters(v)
47
+ v == 4 ? V4_AESV2_FILTERS : {}
48
+ end
49
+
50
+ # Objects whose bytes are never encrypted (both write and read
51
+ # directions consult this): the /Encrypt dictionary itself
52
+ # (s7.6.3 — its values carry the password hashes) and
53
+ # cross-reference streams (always cleartext).
54
+ def exempt_object?(document, obj)
55
+ encrypt_oid = (document.trailer || {})[:Encrypt]&.oid
56
+ obj.oid == encrypt_oid ||
57
+ (obj.is_a?(Pdfrb::Model::Cos::Stream) && obj.value[:Type] == :XRef)
58
+ end
35
59
  end
36
60
  end
37
61
 
@@ -71,7 +71,7 @@ module Pdfrb
71
71
  ordered.each do |obj|
72
72
  offsets[obj.oid] = temp.pos
73
73
  temp << @serializer.serialize_indirect(obj,
74
- skip_string_encryption: encryption_dict?(obj))
74
+ skip_encryption: Pdfrb::Encryption.exempt_object?(@document, obj))
75
75
  end
76
76
  first_page_end = temp.pos if first_page_objects.any?
77
77
 
@@ -133,7 +133,7 @@ module Pdfrb
133
133
 
134
134
  layout.ordered_objects.each do |obj|
135
135
  buf << @serializer.serialize_indirect(obj,
136
- skip_string_encryption: encryption_dict?(obj))
136
+ skip_encryption: Pdfrb::Encryption.exempt_object?(@document, obj))
137
137
  end
138
138
 
139
139
  buf << "#{layout.hint_oid} 0 obj\n"
@@ -261,11 +261,6 @@ module Pdfrb
261
261
  "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"
262
262
  end
263
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
-
269
264
  def write_non_linearized(io)
270
265
  Pdfrb::Writer.write(@document, io)
271
266
  end
@@ -53,24 +53,24 @@ module Pdfrb
53
53
  # header and (for streams) the stream payload. When an encrypter
54
54
  # is set, all strings in the object's dictionaries and the stream
55
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)
56
+ # +skip_encryption:+ for objects that must stay readable: the
57
+ # /Encrypt dictionary itself (s7.6.3) and cross-reference streams.
58
+ def serialize_indirect(obj, skip_encryption: false)
59
59
  raise ArgumentError, "not indirect: #{obj.inspect}" unless obj.indirect?
60
60
 
61
- encrypt_strings = !skip_string_encryption && encrypter
61
+ encrypt_strings = !skip_encryption && encrypter
62
62
  buffer = +""
63
63
  buffer << "#{obj.oid} #{obj.gen} obj\n"
64
64
  case obj
65
65
  when Pdfrb::Model::Cos::Stream
66
- dict_value = encrypt_strings ? encrypt_value_strings(obj.value, obj.oid, obj.gen) : obj.value
66
+ dict_value = encrypt_strings ? Pdfrb::Encryption::ValueStrings.encrypt(obj.value, obj.oid, obj.gen, encrypter) : obj.value
67
67
  dict, payload = emit_stream(obj, dict_value)
68
68
  buffer << dict
69
69
  buffer << "\nstream\n"
70
- buffer << (encrypter ? encrypter.encrypt(payload, obj.oid, obj.gen) : payload)
70
+ buffer << (encrypter && !skip_encryption ? encrypter.encrypt_stream(payload, obj.oid, obj.gen) : payload)
71
71
  buffer << "\nendstream\n"
72
72
  else
73
- value = encrypt_strings ? encrypt_value_strings(obj.value, obj.oid, obj.gen) : obj.value
73
+ value = encrypt_strings ? Pdfrb::Encryption::ValueStrings.encrypt(obj.value, obj.oid, obj.gen, encrypter) : obj.value
74
74
  buffer << serialize(value)
75
75
  buffer << "\n"
76
76
  end
@@ -80,33 +80,6 @@ module Pdfrb
80
80
 
81
81
  private
82
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
-
110
83
  # Build the dict body + payload for a stream object, applying
111
84
  # FlateDecode compression when enabled and the stream is large
112
85
  # enough to benefit. Returns [dict_bytes, payload_bytes].
@@ -29,13 +29,37 @@ module Pdfrb
29
29
  return nil if entry.nil? || entry.free?
30
30
 
31
31
  obj = case entry.type
32
- when :in_use then parse_at(entry.offset, oid, entry.gen)
32
+ when :in_use then decrypt_loaded(parse_at(entry.offset, oid, entry.gen), entry.gen)
33
33
  when :compressed then load_from_objstm(entry.obj_stm_oid, entry.index, oid)
34
34
  end
35
35
  @cache[oid] = obj
36
36
  obj
37
37
  end
38
38
 
39
+ # The single funnel for read-side decryption (s7.6.1): strings
40
+ # in the object's dictionaries and the stream payload decrypt
41
+ # with the per-object key as the object enters the Model.
42
+ # Objects inside object streams need no separate treatment —
43
+ # only the containing ObjStm stream is encrypted — and the
44
+ # /Encrypt dictionary is exempt (s7.6.3).
45
+ def decrypt_loaded(obj, gen)
46
+ return obj if obj.nil?
47
+ # The exemption check must precede handler construction:
48
+ # building the handler resolves the /Encrypt dict through
49
+ # this same funnel, which would recurse before the memo is
50
+ # set.
51
+ return obj if Pdfrb::Encryption.exempt_object?(document, obj)
52
+
53
+ handler = document.encryption.reader_handler
54
+ return obj if handler.nil?
55
+
56
+ Pdfrb::Encryption::ValueStrings.decrypt!(obj.value, obj.oid, gen, handler)
57
+ if obj.is_a?(Pdfrb::Model::Cos::Stream) && obj.stream
58
+ obj.stream = handler.decrypt_stream(obj.stream, obj.oid, gen)
59
+ end
60
+ obj
61
+ end
62
+
39
63
  def clear_cache
40
64
  @cache.clear
41
65
  @objstm_cache.clear
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.7"
4
+ VERSION = "0.8.8"
5
5
  end
data/lib/pdfrb/writer.rb CHANGED
@@ -74,12 +74,6 @@ module Pdfrb
74
74
  { encrypter: handler }
75
75
  end
76
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
81
- end
82
-
83
77
  def self.write(document, io)
84
78
  new(document, io).write
85
79
  end
@@ -104,7 +98,7 @@ module Pdfrb
104
98
 
105
99
  @xref_offsets[obj.oid] = @io.pos
106
100
  @io << @serializer.serialize_indirect(obj,
107
- skip_string_encryption: encryption_dict?(obj))
101
+ skip_encryption: Pdfrb::Encryption.exempt_object?(document, obj))
108
102
  end
109
103
 
110
104
  xref_pos = if use_stream
@@ -139,7 +133,7 @@ module Pdfrb
139
133
 
140
134
  @xref_offsets[obj.oid] = @io.pos
141
135
  @io << @serializer.serialize_indirect(obj,
142
- skip_string_encryption: encryption_dict?(obj))
136
+ skip_encryption: Pdfrb::Encryption.exempt_object?(document, obj))
143
137
  end
144
138
  xref_pos = write_xref(prev: previous_xref_offset)
145
139
  write_trailer(xref_pos, prev: previous_xref_offset)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pdfrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.7
4
+ version: 0.8.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-30 00:00:00.000000000 Z
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark
@@ -891,6 +891,7 @@ files:
891
891
  - lib/pdfrb/encryption/security_handler.rb
892
892
  - lib/pdfrb/encryption/standard_security_handler.rb
893
893
  - lib/pdfrb/encryption/v5_writer.rb
894
+ - lib/pdfrb/encryption/value_strings.rb
894
895
  - lib/pdfrb/error.rb
895
896
  - lib/pdfrb/filter.rb
896
897
  - lib/pdfrb/filter/ascii_85_decode.rb