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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29668fa0363922e98c020c762e0de36fc221ed3667407d70ab68b02bc40bf90e
4
- data.tar.gz: 10e2c7a8f2f9dd8e43b464cab777ef54e921004ae8a59b89c3c29dfd3c0823db
3
+ metadata.gz: '0117148def4f7bc7280fa9727a6ce26fcafa9cb8f6e31cb32a480cb9548ac99e'
4
+ data.tar.gz: a7bf629aabbef013625f56e4c719f97a4fa6bc2823c158d4d7f37ea9c6ff8379
5
5
  SHA512:
6
- metadata.gz: 5b2e9643a1eb44228dccaf6882139060297ebb0bfec23ba33f5e10303d4c477dd562a94b98bc0027ed049f99aac94bf56eae0cf19160cd37ed78825e32422d41
7
- data.tar.gz: c43041d9ca87df1184b44897a4f5456e3b252e1a3cd604d67d2b6d6d8a1e0c4d4327c6ee01a449f1f16f7b8527098f1b73b7f0132b8aa148393f484617d161e8
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
@@ -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
- 20.times { |i| hash = rc4.process(hash, key ^ i) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.7.14"
4
+ VERSION = "0.7.15"
5
5
  end
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.14
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