pdfrb 0.8.6 → 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 +4 -4
- data/lib/pdfrb/cli.rb +11 -4
- data/lib/pdfrb/digital_signature.rb +8 -7
- data/lib/pdfrb/document/encryption.rb +31 -2
- data/lib/pdfrb/encryption/identity.rb +4 -0
- data/lib/pdfrb/encryption/password_verification.rb +14 -10
- data/lib/pdfrb/encryption/public_key_security_handler.rb +16 -0
- data/lib/pdfrb/encryption/standard_security_handler.rb +85 -9
- data/lib/pdfrb/encryption/value_strings.rb +59 -0
- data/lib/pdfrb/encryption.rb +24 -0
- data/lib/pdfrb/linearization/writer.rb +15 -5
- data/lib/pdfrb/serializer.rb +14 -7
- data/lib/pdfrb/source/object_reader.rb +25 -1
- data/lib/pdfrb/version.rb +1 -1
- data/lib/pdfrb/writer.rb +27 -12
- data/lib/pdfrb.rb +0 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af5b6aea283bcc7325c584008268aaedc228f78882cb5408b615c55576cb8154
|
|
4
|
+
data.tar.gz: 29c3a794f7829144e4fa2275ba4a7b4124087abd7279f076689b06d14d5ac4c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6491c0a75e0ef6eb6fa849fdf25de0d546c2055b099e3f533595a073e370a30c37693604b31620e5d3707237e1960f18f6ba7ee05555698c21b081e6300cf493
|
|
7
|
+
data.tar.gz: 569d900685cc748edba60dfcdc426987343025c4ccae008a715b188ce41ce9d56abf29901debb39e1292d856ba34951394b85a57efb297ac9b5af0b67f706eb6
|
data/lib/pdfrb/cli.rb
CHANGED
|
@@ -325,19 +325,26 @@ 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
|
|
334
335
|
|
|
335
336
|
# Set up the handler so Writer can encrypt per-object.
|
|
336
337
|
handler = Pdfrb::Encryption::StandardSecurityHandler.new(
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
{
|
|
339
|
+
Encrypt: { V: v, R: r, Length: length, P: perms,
|
|
340
|
+
O: o_entry, U: u_entry, **cf_entries },
|
|
341
|
+
ID: [id0.b, id0.b],
|
|
342
|
+
}
|
|
339
343
|
)
|
|
340
|
-
handler.verify_user_password(user_pw)
|
|
344
|
+
unless handler.verify_user_password?(user_pw)
|
|
345
|
+
raise Pdfrb::EncryptionError, "failed to derive encryption key"
|
|
346
|
+
end
|
|
347
|
+
|
|
341
348
|
doc.config["encryption.handler"] = handler
|
|
342
349
|
doc.config["encryption.password"] = user_pw
|
|
343
350
|
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
|
-
|
|
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:, **)
|
|
@@ -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):
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
+
hash = RC4.new(key).process(hash)
|
|
114
114
|
end
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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(
|
|
317
|
+
cipher = OpenSSL::Cipher.new(aes_cipher_name(key))
|
|
242
318
|
cipher.encrypt
|
|
243
|
-
cipher.key = key
|
|
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(
|
|
330
|
+
decipher = OpenSSL::Cipher.new(aes_cipher_name(key))
|
|
255
331
|
decipher.decrypt
|
|
256
|
-
decipher.key = key
|
|
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
|
data/lib/pdfrb/encryption.rb
CHANGED
|
@@ -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
|
|
|
@@ -30,7 +30,7 @@ module Pdfrb
|
|
|
30
30
|
|
|
31
31
|
def initialize(document)
|
|
32
32
|
@document = document
|
|
33
|
-
@serializer = Pdfrb::
|
|
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_encryption: Pdfrb::Encryption.exempt_object?(@document, 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_encryption: Pdfrb::Encryption.exempt_object?(@document, 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
|
-
|
|
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
|
|
data/lib/pdfrb/serializer.rb
CHANGED
|
@@ -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
|
-
|
|
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_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)
|
|
55
59
|
raise ArgumentError, "not indirect: #{obj.inspect}" unless obj.indirect?
|
|
56
60
|
|
|
61
|
+
encrypt_strings = !skip_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
|
-
|
|
66
|
+
dict_value = encrypt_strings ? Pdfrb::Encryption::ValueStrings.encrypt(obj.value, obj.oid, obj.gen, encrypter) : obj.value
|
|
67
|
+
dict, payload = emit_stream(obj, dict_value)
|
|
62
68
|
buffer << dict
|
|
63
69
|
buffer << "\nstream\n"
|
|
64
|
-
buffer << (encrypter ? encrypter.
|
|
70
|
+
buffer << (encrypter && !skip_encryption ? encrypter.encrypt_stream(payload, obj.oid, obj.gen) : payload)
|
|
65
71
|
buffer << "\nendstream\n"
|
|
66
72
|
else
|
|
67
|
-
|
|
73
|
+
value = encrypt_strings ? Pdfrb::Encryption::ValueStrings.encrypt(obj.value, obj.oid, obj.gen, encrypter) : obj.value
|
|
74
|
+
buffer << serialize(value)
|
|
68
75
|
buffer << "\n"
|
|
69
76
|
end
|
|
70
77
|
buffer << "endobj\n"
|
|
@@ -76,9 +83,9 @@ module Pdfrb
|
|
|
76
83
|
# Build the dict body + payload for a stream object, applying
|
|
77
84
|
# FlateDecode compression when enabled and the stream is large
|
|
78
85
|
# enough to benefit. Returns [dict_bytes, payload_bytes].
|
|
79
|
-
def emit_stream(stream_obj)
|
|
86
|
+
def emit_stream(stream_obj, dict_value = stream_obj.value)
|
|
80
87
|
payload = stream_obj.stream
|
|
81
|
-
hash =
|
|
88
|
+
hash = dict_value.dup
|
|
82
89
|
existing_filter = hash[:Filter]
|
|
83
90
|
|
|
84
91
|
if should_compress?(stream_obj, payload, existing_filter)
|
|
@@ -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
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 =
|
|
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
|
-
**
|
|
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
|
|
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,15 @@ 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
|
-
{}
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
def self.write(document, io)
|
|
@@ -84,7 +97,8 @@ module Pdfrb
|
|
|
84
97
|
next if packed.key?(obj.oid)
|
|
85
98
|
|
|
86
99
|
@xref_offsets[obj.oid] = @io.pos
|
|
87
|
-
@io << @serializer.serialize_indirect(obj
|
|
100
|
+
@io << @serializer.serialize_indirect(obj,
|
|
101
|
+
skip_encryption: Pdfrb::Encryption.exempt_object?(document, obj))
|
|
88
102
|
end
|
|
89
103
|
|
|
90
104
|
xref_pos = if use_stream
|
|
@@ -118,7 +132,8 @@ module Pdfrb
|
|
|
118
132
|
next unless obj.indirect?
|
|
119
133
|
|
|
120
134
|
@xref_offsets[obj.oid] = @io.pos
|
|
121
|
-
@io << @serializer.serialize_indirect(obj
|
|
135
|
+
@io << @serializer.serialize_indirect(obj,
|
|
136
|
+
skip_encryption: Pdfrb::Encryption.exempt_object?(document, obj))
|
|
122
137
|
end
|
|
123
138
|
xref_pos = write_xref(prev: previous_xref_offset)
|
|
124
139
|
write_trailer(xref_pos, prev: previous_xref_offset)
|
|
@@ -293,7 +308,7 @@ module Pdfrb
|
|
|
293
308
|
existing = document.trailer || {}
|
|
294
309
|
trailer_hash = {}
|
|
295
310
|
existing.each do |k, v|
|
|
296
|
-
next if
|
|
311
|
+
next if TRAILER_OVERRIDDEN_KEYS.include?(k)
|
|
297
312
|
|
|
298
313
|
trailer_hash[k] = v
|
|
299
314
|
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,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pdfrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
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-
|
|
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
|