pdfrb 0.7.14 → 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 +4 -4
- data/lib/pdfrb/document/encryption.rb +205 -0
- data/lib/pdfrb/document.rb +3 -0
- data/lib/pdfrb/encryption/standard_security_handler.rb +5 -1
- data/lib/pdfrb/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0117148def4f7bc7280fa9727a6ce26fcafa9cb8f6e31cb32a480cb9548ac99e'
|
|
4
|
+
data.tar.gz: a7bf629aabbef013625f56e4c719f97a4fa6bc2823c158d4d7f37ea9c6ff8379
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7d17104eaf145c31e843f94e622dd181b965f18aa4e3aba4adb80bedbdb500855984c603ef2ce60fa4abec31d1e4a07841d96397a71452147a6bff19c122b94
|
|
7
|
+
data.tar.gz: e3513e197dff0e244f97adff17e332ff74b75509054449c32a779e8926bf1fbb91f9965b8a5757f78dbeb54f5e09741260b4139aef31f79cb0caaa25ca878b17
|
|
@@ -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
|
data/lib/pdfrb/document.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/pdfrb/version.rb
CHANGED
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.
|
|
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
|