combine_pdf 0.2.5 → 0.2.37

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,258 +5,268 @@
5
5
  ## is subject to the same license.
6
6
  ########################################################
7
7
 
8
+ module CombinePDF
9
+ #:nodoc: all
8
10
 
11
+ protected
9
12
 
10
- module CombinePDF
11
- #:nodoc: all
13
+ # @private
14
+ # @!visibility private
12
15
 
13
- protected
14
-
15
- # @private
16
- # @!visibility private
16
+ # This is an internal class. you don't need it.
17
+ class PDFDecrypt
18
+ # include CombinePDF::Renderer
17
19
 
18
- # This is an internal class. you don't need it.
19
- class PDFDecrypt
20
- include CombinePDF::Renderer
20
+ # @!visibility private
21
21
 
22
- # @!visibility private
23
-
24
- # make a new Decrypt object. requires:
25
- # objects:: an array containing the encrypted objects.
26
- # root_dictionary:: the root PDF dictionary, containing the Encrypt dictionary.
27
- def initialize objects=[], root_dictionary = {}
28
- @objects = objects
29
- @encryption_dictionary = actual_object(root_dictionary[:Encrypt])
30
- raise "Cannot decrypt an encrypted file without an encryption dictionary!" unless @encryption_dictionary
31
- @root_dictionary = actual_object(root_dictionary)
32
- @padding_key = [ 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
33
- 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
34
- 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
35
- 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A ]
36
- @key_crypt_first_iv_store = nil
37
- @encryption_iv = nil
38
- change_references_to_actual_values @encryption_dictionary
39
- end
22
+ # make a new Decrypt object. requires:
23
+ # objects:: an array containing the encrypted objects.
24
+ # root_dictionary:: the root PDF dictionary, containing the Encrypt dictionary.
25
+ def initialize(objects = [], root_dictionary = {})
26
+ @objects = objects
27
+ @encryption_dictionary = actual_object(root_dictionary[:Encrypt])
28
+ raise EncryptionError, 'Cannot decrypt an encrypted file without an encryption dictionary!' unless @encryption_dictionary
29
+ @root_dictionary = actual_object(root_dictionary)
30
+ @padding_key = [0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
31
+ 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
32
+ 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
33
+ 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]
40
34
 
41
- # call this to start the decryption.
42
- def decrypt
43
- raise_encrypted_error @encryption_dictionary unless @encryption_dictionary[:Filter] == :Standard
44
- @key = set_general_key
45
- case actual_object(@encryption_dictionary[:V])
46
- when 1,2
47
- # raise_encrypted_error
48
- _perform_decrypt_proc_ @objects, self.method(:decrypt_RC4)
49
- when 4
50
- # raise unsupported error for now
51
- raise_encrypted_error
52
- # make sure CF is a Hash (as required by the PDF standard for this type of encryption).
53
- raise_encrypted_error unless actual_object(@encryption_dictionary[:CF]).is_a?(Hash)
35
+ change_references_to_actual_values @encryption_dictionary
36
+ end
54
37
 
55
- # do nothing if there is no data to decrypt except embeded files...?
56
- return true unless (actual_object(@encryption_dictionary[:CF]).values.select { |v| !v[:AuthEvent] || v[:AuthEvent] == :DocOpen } ).empty?
38
+ # call this to start the decryption.
39
+ def decrypt
40
+ raise_encrypted_error @encryption_dictionary unless @encryption_dictionary[:Filter] == :Standard
41
+ @key = set_general_key
42
+ case actual_object(@encryption_dictionary[:V])
43
+ when 1, 2
44
+ # raise_encrypted_error
45
+ _perform_decrypt_proc_ @objects, method(:decrypt_RC4)
46
+ when 4
47
+ # make sure CF is a Hash (as required by the PDF standard for this type of encryption).
48
+ raise_encrypted_error unless actual_object(@encryption_dictionary[:CF]).is_a?(Hash)
57
49
 
58
- # attempt to decrypt all strings?
59
- # attempt to decrypy all streams
60
- # attempt to decrypt all embeded files?
50
+ # support trivial case for now
51
+ # - same filter for streams (Stmf) and strings(Strf)
52
+ # - AND :CFM == :V2 (use algorithm 1)
53
+ raise_encrypted_error unless (@encryption_dictionary[:StmF] == @encryption_dictionary[:StrF])
61
54
 
62
- else
63
- raise_encrypted_error
64
- end
65
- #rebuild stream lengths?
66
- @objects
67
- rescue => e
68
- raise_encrypted_error
69
- end
55
+ cfilter = actual_object(@encryption_dictionary[:CF])[@encryption_dictionary[:StrF]]
56
+ raise_encrypted_error unless cfilter
57
+ raise_encrypted_error unless (cfilter[:AuthEvent] == :DocOpen)
58
+ if (cfilter[:CFM] == :V2)
59
+ _perform_decrypt_proc_ @objects, method(:decrypt_RC4)
60
+ elsif (cfilter[:CFM] == :AESV2)
61
+ _perform_decrypt_proc_ @objects, method(:decrypt_AES)
62
+ else
63
+ raise_encrypted_error
64
+ end
65
+ end
66
+ # rebuild stream lengths?
67
+ @objects
68
+ rescue => e
69
+ puts e
70
+ puts e.message
71
+ puts e.backtrace.join("\n")
72
+ raise_encrypted_error
73
+ end
70
74
 
71
- protected
75
+ protected
72
76
 
73
- def set_general_key(password = "")
74
- # 1) make sure the initial key is 32 byte long (if no password, uses padding).
75
- key = (password.bytes[0..32].to_a + @padding_key)[0..31].to_a.pack('C*').force_encoding(Encoding::ASCII_8BIT)
76
- # 2) add the value of the encryption dictionary’s O entry
77
- key << actual_object(@encryption_dictionary[:O]).to_s
78
- # 3) Convert the integer value of the P entry to a 32-bit unsigned binary number
79
- # and pass these bytes low-order byte first
80
- key << [actual_object(@encryption_dictionary[:P])].pack('i')
81
- # 4) Pass the first element of the file’s file identifier array
82
- # (the value of the ID entry in the document’s trailer dictionary
83
- key << actual_object(@root_dictionary[:ID])[0]
84
- # # 4(a) (Security handlers of revision 4 or greater)
85
- # # if document metadata is not being encrypted, add 4 bytes with the value 0xFFFFFFFF.
86
- if actual_object(@encryption_dictionary[:R]) >= 4
87
- unless actual_object(@encryption_dictionary)[:EncryptMetadata] == false #default is true and nil != false
88
- key << "\x00\x00\x00\x00"
89
- else
90
- key << "\xFF\xFF\xFF\xFF"
91
- end
92
- end
93
- # 5) pass everything as a MD5 hash
94
- key = Digest::MD5.digest(key)
95
- # 5(a) h) (Security handlers of revision 3 or greater) Do the following 50 times:
96
- # Take the output from the previous MD5 hash and
97
- # pass the first n bytes of the output as input into a new MD5 hash,
98
- # where n is the number of bytes of the encryption key as defined by the value of
99
- # the encryption dictionary’s Length entry.
100
- if actual_object(@encryption_dictionary[:R]) >= 3
101
- 50.times do|i|
102
- key = Digest::MD5.digest(key[0...actual_object(@encryption_dictionary[:Length])])
103
- end
104
- end
105
- # 6) Set the encryption key to the first n bytes of the output from the final MD5 hash,
106
- # where n shall always be 5 for security handlers of revision 2 but,
107
- # for security handlers of revision 3 or greater,
108
- # shall depend on the value of the encryption dictionary’s Length entry.
109
- if actual_object(@encryption_dictionary[:R]) >= 3
110
- @key = key[0..(actual_object(@encryption_dictionary[:Length])/8)]
111
- else
112
- @key = key[0..4]
113
- end
114
- @key
115
- end
116
- def decrypt_none(encrypted, encrypted_id, encrypted_generation, encrypted_filter)
117
- "encrypted"
118
- end
119
- def decrypt_RC4(encrypted, encrypted_id, encrypted_generation, encrypted_filter)
120
- ## start decryption using padding strings
121
- object_key = @key.dup
122
- object_key << [encrypted_id].pack('i')[0..2]
123
- object_key << [encrypted_generation].pack('i')[0..1]
124
- # (0..2).each { |e| object_key << (encrypted_id >> e*8 & 0xFF ) }
125
- # (0..1).each { |e| object_key << (encrypted_generation >> e*8 & 0xFF ) }
126
- key_length = object_key.length < 16 ? object_key.length : 16
127
- rc4 = ::RC4.new( Digest::MD5.digest(object_key)[(0...key_length)] )
128
- rc4.decrypt(encrypted)
129
- end
130
- def decrypt_AES(encrypted, encrypted_id, encrypted_generation, encrypted_filter)
131
- ## extract encryption_iv if it wasn't extracted yet
132
- unless @encryption_iv
133
- @encryption_iv = encrypted[0..15].to_i
134
- #raise "Tryed decrypting using AES and couldn't extract iv" if @encryption_iv == 0
135
- @encryption_iv = 0.chr * 16
136
- #encrypted = encrypted[16..-1]
137
- end
138
- ## start decryption using padding strings
139
- object_key = @key.dup
140
- (0..2).each { |e| object_key << (encrypted_id >> e*8 & 0xFF ) }
141
- (0..1).each { |e| object_key << (encrypted_generation >> e*8 & 0xFF ) }
142
- object_key << "sAlT"
143
- key_length = object_key.length < 16 ? object_key.length : 16
144
- cipher = OpenSSL::Cipher::Cipher.new("aes-#{object_key.length << 3}-cbc").decrypt
145
- cipher.padding = 0
146
- (cipher.update(encrypted) + cipher.final).unpack("C*")
147
- end
77
+ def set_general_key(password = '')
78
+ # 1) make sure the initial key is 32 byte long (if no password, uses padding).
79
+ key = (password.bytes[0..32].to_a + @padding_key)[0..31].to_a.pack('C*').force_encoding(Encoding::ASCII_8BIT)
80
+ # 2) add the value of the encryption dictionary’s O entry
81
+ key << actual_object(@encryption_dictionary[:O]).to_s
82
+ # 3) Convert the integer value of the P entry to a 32-bit unsigned binary number
83
+ # and pass these bytes low-order byte first
84
+ key << [actual_object(@encryption_dictionary[:P])].pack('i')
85
+ # 4) Pass the first element of the file’s file identifier array
86
+ # (the value of the ID entry in the document’s trailer dictionary
87
+ key << actual_object(@root_dictionary[:ID])[0]
88
+ # # 4(a) (Security handlers of revision 4 or greater)
89
+ # # if document metadata is not being encrypted, add 4 bytes with the value 0xFFFFFFFF.
90
+ if actual_object(@encryption_dictionary[:R]) >= 4
91
+ if actual_object(@encryption_dictionary)[:EncryptMetadata] == false
92
+ key << "\xFF\xFF\xFF\xFF".force_encoding(Encoding::ASCII_8BIT)
93
+ end
94
+ end
95
+ # 5) pass everything as a MD5 hash
96
+ key = Digest::MD5.digest(key)
97
+ # 5(a) h) (Security handlers of revision 3 or greater) Do the following 50 times:
98
+ # Take the output from the previous MD5 hash and
99
+ # pass the first n bytes of the output as input into a new MD5 hash,
100
+ # where n is the number of bytes of the encryption key as defined by the value of
101
+ # the encryption dictionary’s Length entry.
102
+ if actual_object(@encryption_dictionary[:R]) >= 3
103
+ 50.times do |_i|
104
+ key = Digest::MD5.digest(key[0...actual_object(@encryption_dictionary[:Length])])
105
+ end
106
+ end
107
+ # 6) Set the encryption key to the first n bytes of the output from the final MD5 hash,
108
+ # where n shall always be 5 for security handlers of revision 2 but,
109
+ # for security handlers of revision 3 or greater,
110
+ # shall depend on the value of the encryption dictionary’s Length entry.
111
+ @key = if actual_object(@encryption_dictionary[:R]) >= 3
112
+ key[0..(actual_object(@encryption_dictionary[:Length]) / 8)]
113
+ else
114
+ key[0..4]
115
+ end
116
+ @key
117
+ end
148
118
 
149
- protected
119
+ def decrypt_none(_encrypted, _encrypted_id, _encrypted_generation, _encrypted_filter)
120
+ 'encrypted'
121
+ end
150
122
 
151
- def _perform_decrypt_proc_ (object, decrypt_proc, encrypted_id = nil, encrypted_generation = nil, encrypted_filter = nil)
152
- case
153
- when object.is_a?(Array)
154
- object.map! { |item| _perform_decrypt_proc_(item, decrypt_proc, encrypted_id, encrypted_generation, encrypted_filter) }
155
- when object.is_a?(Hash)
156
- encrypted_id ||= actual_object(object[:indirect_reference_id])
157
- encrypted_generation ||= actual_object(object[:indirect_generation_number])
158
- encrypted_filter ||= actual_object(object[:Filter])
159
- if object[:raw_stream_content]
160
- stream_length = actual_object(object[:Length])
161
- actual_length = object[:raw_stream_content].length
162
- warn "Stream registeded length was #{object[:Length].to_s} and the actual length was #{actual_length}." if actual_length < stream_length
163
- length = [ stream_length, actual_length].min
164
- object[:raw_stream_content] = decrypt_proc.call( (object[:raw_stream_content][0...length]), encrypted_id, encrypted_generation, encrypted_filter)
165
- end
166
- object.each {|k, v| object[k] = _perform_decrypt_proc_(v, decrypt_proc, encrypted_id, encrypted_generation, encrypted_filter) if k != :raw_stream_content && (v.is_a?(Hash) || v.is_a?(Array) || v.is_a?(String))} # assumes no decrypting is ever performed on keys
167
- when object.is_a?(String)
168
- return decrypt_proc.call(object, encrypted_id, encrypted_generation, encrypted_filter)
169
- else
170
- return object
171
- end
172
-
173
- end
123
+ def decrypt_RC4(encrypted, encrypted_id, encrypted_generation, _encrypted_filter)
124
+ ## start decryption using padding strings
125
+ object_key = @key.dup
126
+ object_key << [encrypted_id].pack('i')[0..2]
127
+ object_key << [encrypted_generation].pack('i')[0..1]
128
+ # (0..2).each { |e| object_key << (encrypted_id >> e*8 & 0xFF ) }
129
+ # (0..1).each { |e| object_key << (encrypted_generation >> e*8 & 0xFF ) }
130
+ key_length = object_key.length < 16 ? object_key.length : 16
131
+ rc4 = ::RC4.new(Digest::MD5.digest(object_key)[(0...key_length)])
132
+ rc4.decrypt(encrypted)
133
+ end
174
134
 
175
- def raise_encrypted_error object = nil
176
- object ||= @encryption_dictionary.to_s.split(',').join("\n")
177
- warn "Data raising exception:\n #{object.to_s.split(',').join("\n")}"
178
- raise "File is encrypted - not supported."
179
- end
135
+ def decrypt_AES(encrypted, encrypted_id, encrypted_generation, _encrypted_filter)
136
+ ## start decryption using padding strings
137
+ object_key = @key.dup
138
+ object_key << [encrypted_id].pack('i')[0..2]
139
+ object_key << [encrypted_generation].pack('i')[0..1]
140
+ object_key << 'sAlT'.force_encoding(Encoding::ASCII_8BIT)
141
+ key_length = object_key.length < 16 ? object_key.length : 16
180
142
 
181
- def change_references_to_actual_values(hash_with_references = {})
182
- hash_with_references.each do |k,v|
183
- if v.is_a?(Hash) && v[:is_reference_only]
184
- hash_with_references[k] = get_refernced_object(v)
185
- hash_with_references[k] = hash_with_references[k][:indirect_without_dictionary] if hash_with_references[k].is_a?(Hash) && hash_with_references[k][:indirect_without_dictionary]
186
- warn "Couldn't connect all values from references - didn't find reference #{hash_with_references}!!!" if hash_with_references[k] == nil
187
- hash_with_references[k] = v unless hash_with_references[k]
188
- end
189
- end
190
- hash_with_references
191
- end
192
- def get_refernced_object(reference_hash = {})
193
- @objects.each do |stored_object|
194
- return stored_object if ( stored_object.is_a?(Hash) &&
195
- reference_hash[:indirect_reference_id] == stored_object[:indirect_reference_id] &&
196
- reference_hash[:indirect_generation_number] == stored_object[:indirect_generation_number] )
197
- end
198
- warn "didn't find reference #{reference_hash}"
199
- nil
200
- end
143
+ begin
144
+ cipher = OpenSSL::Cipher.new("aes-#{key_length << 3}-cbc")
145
+ cipher.decrypt
146
+ cipher.key = Digest::MD5.digest(object_key)[(0...key_length)]
147
+ cipher.iv = encrypted[0..15]
148
+ cipher.padding = 0
149
+ cipher.update(encrypted[16..-1]) + cipher.final
150
+ rescue StandardError => e
151
+ # puts e.class.name
152
+ encrypted
153
+ end
154
+ end
201
155
 
156
+ protected
202
157
 
203
- # # returns the PDF Object Hash holding the acutal data (if exists) or the original hash (if it wasn't a reference)
204
- # #
205
- # # works only AFTER references have been connected.
206
- # def get_referenced object
207
- # object[:referenced_object] || object
208
- # end
158
+ def _perform_decrypt_proc_(object, decrypt_proc, encrypted_id = nil, encrypted_generation = nil, encrypted_filter = nil)
159
+ if object.is_a?(Array)
160
+ object.map! { |item| _perform_decrypt_proc_(item, decrypt_proc, encrypted_id, encrypted_generation, encrypted_filter) }
161
+ elsif object.is_a?(Hash)
162
+ encrypted_id ||= actual_object(object[:indirect_reference_id])
163
+ encrypted_generation ||= actual_object(object[:indirect_generation_number])
164
+ encrypted_filter ||= actual_object(object[:Filter])
165
+ if object[:raw_stream_content] && !object[:raw_stream_content].empty?
166
+ stream_length = actual_object(object[:Length])
167
+ actual_length = object[:raw_stream_content].bytesize
168
+ # p stream_length
169
+ # p actual_length
170
+ # p object[:Length]
171
+ # p object
172
+ warn "Stream registered length was #{object[:Length]} and the actual length was #{actual_length}." if actual_length < stream_length
173
+ length = [stream_length, actual_length].min
174
+ object[:raw_stream_content] = decrypt_proc.call((object[:raw_stream_content][0...length]), encrypted_id, encrypted_generation, encrypted_filter)
175
+ end
176
+ object.each { |k, v| object[k] = _perform_decrypt_proc_(v, decrypt_proc, encrypted_id, encrypted_generation, encrypted_filter) if k != :raw_stream_content && (v.is_a?(Hash) || v.is_a?(Array) || v.is_a?(String)) } # assumes no decrypting is never performed on keys
177
+ elsif object.is_a?(String)
178
+ return decrypt_proc.call(object, encrypted_id, encrypted_generation, encrypted_filter)
179
+ else
180
+ return object
181
+ end
182
+ end
209
183
 
210
- end
211
- #####################################################
212
- ## The following isn't my code!!!!
213
- ## It is subject to a different license and copyright.
214
- ## This was the code for the RC4 Gem,
215
- ## ... I had a bad internet connection so I ended up
216
- ## copying it from the web page I had in my cache.
217
- ## This wonderful work was done by Caige Nichols.
218
- #####################################################
219
- # class RC4
220
- # def initialize(str)
221
- # begin
222
- # raise SyntaxError, "RC4: Key supplied is blank" if str.eql?('')
184
+ def raise_encrypted_error(object = nil)
185
+ object ||= @encryption_dictionary.to_s.split(',').join("\n")
186
+ warn "Data raising exception:\n #{object.to_s.split(',').join("\n")}"
187
+ raise EncryptionError, 'File is encrypted - not supported.'
188
+ end
223
189
 
224
- # @q1, @q2 = 0, 0
225
- # @key = []
226
- # str.each_byte { |elem| @key << elem } while @key.size < 256
227
- # @key.slice!(256..@key.size-1) if @key.size >= 256
228
- # @s = (0..255).to_a
229
- # j = 0
230
- # 0.upto(255) do |i|
231
- # j = (j + @s[i] + @key[i] ) % 256
232
- # @s[i], @s[j] = @s[j], @s[i]
233
- # end
234
- # end
235
- # end
190
+ def change_references_to_actual_values(hash_with_references = {})
191
+ hash_with_references.each do |k, v|
192
+ next unless v.is_a?(Hash) && v[:is_reference_only]
193
+ hash_with_references[k] = get_refernced_object(v)
194
+ hash_with_references[k] = hash_with_references[k][:indirect_without_dictionary] if hash_with_references[k].is_a?(Hash) && hash_with_references[k][:indirect_without_dictionary]
195
+ warn "Couldn't connect all values from references - didn't find reference #{hash_with_references}!!!" if hash_with_references[k].nil?
196
+ hash_with_references[k] = v unless hash_with_references[k]
197
+ end
198
+ hash_with_references
199
+ end
236
200
 
237
- # def encrypt!(text)
238
- # process text
239
- # end
201
+ def get_refernced_object(reference_hash = {})
202
+ @objects.each do |stored_object|
203
+ return (stored_object[:indirect_without_dictionary] || stored_object) if stored_object.is_a?(Hash) &&
204
+ reference_hash[:indirect_reference_id] == stored_object[:indirect_reference_id] &&
205
+ reference_hash[:indirect_generation_number] == stored_object[:indirect_generation_number]
206
+ end
207
+ warn "didn't find reference #{reference_hash}"
208
+ nil
209
+ end
240
210
 
241
- # def encrypt(text)
242
- # process text.dup
243
- # end
211
+ def actual_object(obj)
212
+ return get_refernced_object(obj) if obj.is_a?(Hash) && obj[:indirect_reference_id]
213
+ obj
214
+ end
244
215
 
245
- # alias_method :decrypt, :encrypt
216
+ # # returns the PDF Object Hash holding the acutal data (if exists) or the original hash (if it wasn't a reference)
217
+ # #
218
+ # # works only AFTER references have been connected.
219
+ # def get_referenced object
220
+ # object[:referenced_object] || object
221
+ # end
222
+ end
223
+ #####################################################
224
+ ## The following isn't my code!!!!
225
+ ## It is subject to a different license and copyright.
226
+ ## This was the code for the RC4 Gem,
227
+ ## ... I had a bad internet connection so I ended up
228
+ ## copying it from the web page I had in my cache.
229
+ ## This wonderful work was done by Caige Nichols.
230
+ #####################################################
231
+ # class RC4
232
+ # def initialize(str)
233
+ # begin
234
+ # raise SyntaxError, "RC4: Key supplied is blank" if str.eql?('')
246
235
 
247
- # private
236
+ # @q1, @q2 = 0, 0
237
+ # @key = []
238
+ # str.each_byte { |elem| @key << elem } while @key.size < 256
239
+ # @key.slice!(256..@key.size-1) if @key.size >= 256
240
+ # @s = (0..255).to_a
241
+ # j = 0
242
+ # 0.upto(255) do |i|
243
+ # j = (j + @s[i] + @key[i] ) % 256
244
+ # @s[i], @s[j] = @s[j], @s[i]
245
+ # end
246
+ # end
247
+ # end
248
248
 
249
- # def process(text)
250
- # text.unpack("C*").map { |c| c ^ round }.pack("C*")
251
- # end
249
+ # def encrypt!(text)
250
+ # process text
251
+ # end
252
252
 
253
- # def round
254
- # @q1 = (@q1 + 1) % 256
255
- # @q2 = (@q2 + @s[@q1]) % 256
256
- # @s[@q1], @s[@q2] = @s[@q2], @s[@q1]
257
- # @s[(@s[@q1]+@s[@q2]) % 256]
258
- # end
259
- # end
253
+ # def encrypt(text)
254
+ # process text.dup
255
+ # end
260
256
 
261
- end
257
+ # alias_method :decrypt, :encrypt
262
258
 
259
+ # private
260
+
261
+ # def process(text)
262
+ # text.unpack("C*").map { |c| c ^ round }.pack("C*")
263
+ # end
264
+
265
+ # def round
266
+ # @q1 = (@q1 + 1) % 256
267
+ # @q2 = (@q2 + @s[@q1]) % 256
268
+ # @s[@q1], @s[@q2] = @s[@q2], @s[@q1]
269
+ # @s[(@s[@q1]+@s[@q2]) % 256]
270
+ # end
271
+ # end
272
+ end
@@ -0,0 +1,4 @@
1
+ module CombinePDF
2
+ class EncryptionError < StandardError
3
+ end
4
+ end