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 +4 -4
- data/lib/pdfrb/cli.rb +3 -2
- 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 +2 -7
- data/lib/pdfrb/serializer.rb +7 -34
- data/lib/pdfrb/source/object_reader.rb +25 -1
- data/lib/pdfrb/version.rb +1 -1
- data/lib/pdfrb/writer.rb +2 -8
- 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,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):
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/pdfrb/serializer.rb
CHANGED
|
@@ -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
|
-
# +
|
|
57
|
-
#
|
|
58
|
-
def serialize_indirect(obj,
|
|
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 = !
|
|
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 ?
|
|
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.
|
|
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 ?
|
|
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
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|