origamindee 3.0.0

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.
Files changed (139) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +89 -0
  3. data/COPYING.LESSER +165 -0
  4. data/README.md +131 -0
  5. data/bin/config/pdfcop.conf.yml +236 -0
  6. data/bin/pdf2pdfa +87 -0
  7. data/bin/pdf2ruby +333 -0
  8. data/bin/pdfcop +476 -0
  9. data/bin/pdfdecompress +97 -0
  10. data/bin/pdfdecrypt +91 -0
  11. data/bin/pdfencrypt +113 -0
  12. data/bin/pdfexplode +223 -0
  13. data/bin/pdfextract +277 -0
  14. data/bin/pdfmetadata +143 -0
  15. data/bin/pdfsh +12 -0
  16. data/bin/shell/console.rb +128 -0
  17. data/bin/shell/hexdump.rb +59 -0
  18. data/bin/shell/irbrc +69 -0
  19. data/examples/README.md +34 -0
  20. data/examples/attachments/attachment.rb +38 -0
  21. data/examples/attachments/nested_document.rb +51 -0
  22. data/examples/encryption/encryption.rb +28 -0
  23. data/examples/events/events.rb +72 -0
  24. data/examples/flash/flash.rb +37 -0
  25. data/examples/flash/helloworld.swf +0 -0
  26. data/examples/forms/javascript.rb +54 -0
  27. data/examples/forms/xfa.rb +115 -0
  28. data/examples/javascript/hello_world.rb +22 -0
  29. data/examples/javascript/js_emulation.rb +54 -0
  30. data/examples/loop/goto.rb +32 -0
  31. data/examples/loop/named.rb +33 -0
  32. data/examples/signature/signature.rb +65 -0
  33. data/examples/uri/javascript.rb +56 -0
  34. data/examples/uri/open-uri.rb +21 -0
  35. data/examples/uri/submitform.rb +47 -0
  36. data/lib/origami/3d.rb +364 -0
  37. data/lib/origami/acroform.rb +321 -0
  38. data/lib/origami/actions.rb +318 -0
  39. data/lib/origami/annotations.rb +711 -0
  40. data/lib/origami/array.rb +242 -0
  41. data/lib/origami/boolean.rb +90 -0
  42. data/lib/origami/catalog.rb +418 -0
  43. data/lib/origami/collections.rb +144 -0
  44. data/lib/origami/compound.rb +161 -0
  45. data/lib/origami/destinations.rb +252 -0
  46. data/lib/origami/dictionary.rb +192 -0
  47. data/lib/origami/encryption.rb +1084 -0
  48. data/lib/origami/extensions/fdf.rb +347 -0
  49. data/lib/origami/extensions/ppklite.rb +422 -0
  50. data/lib/origami/filespec.rb +197 -0
  51. data/lib/origami/filters/ascii.rb +211 -0
  52. data/lib/origami/filters/ccitt/tables.rb +267 -0
  53. data/lib/origami/filters/ccitt.rb +357 -0
  54. data/lib/origami/filters/crypt.rb +38 -0
  55. data/lib/origami/filters/dct.rb +54 -0
  56. data/lib/origami/filters/flate.rb +69 -0
  57. data/lib/origami/filters/jbig2.rb +57 -0
  58. data/lib/origami/filters/jpx.rb +47 -0
  59. data/lib/origami/filters/lzw.rb +170 -0
  60. data/lib/origami/filters/predictors.rb +292 -0
  61. data/lib/origami/filters/runlength.rb +129 -0
  62. data/lib/origami/filters.rb +364 -0
  63. data/lib/origami/font.rb +196 -0
  64. data/lib/origami/functions.rb +79 -0
  65. data/lib/origami/graphics/colors.rb +230 -0
  66. data/lib/origami/graphics/instruction.rb +98 -0
  67. data/lib/origami/graphics/path.rb +182 -0
  68. data/lib/origami/graphics/patterns.rb +174 -0
  69. data/lib/origami/graphics/render.rb +62 -0
  70. data/lib/origami/graphics/state.rb +149 -0
  71. data/lib/origami/graphics/text.rb +225 -0
  72. data/lib/origami/graphics/xobject.rb +918 -0
  73. data/lib/origami/graphics.rb +38 -0
  74. data/lib/origami/header.rb +75 -0
  75. data/lib/origami/javascript.rb +713 -0
  76. data/lib/origami/linearization.rb +330 -0
  77. data/lib/origami/metadata.rb +172 -0
  78. data/lib/origami/name.rb +135 -0
  79. data/lib/origami/null.rb +65 -0
  80. data/lib/origami/numeric.rb +181 -0
  81. data/lib/origami/obfuscation.rb +245 -0
  82. data/lib/origami/object.rb +760 -0
  83. data/lib/origami/optionalcontent.rb +183 -0
  84. data/lib/origami/outline.rb +54 -0
  85. data/lib/origami/outputintents.rb +85 -0
  86. data/lib/origami/page.rb +722 -0
  87. data/lib/origami/parser.rb +269 -0
  88. data/lib/origami/parsers/fdf.rb +56 -0
  89. data/lib/origami/parsers/pdf/lazy.rb +176 -0
  90. data/lib/origami/parsers/pdf/linear.rb +122 -0
  91. data/lib/origami/parsers/pdf.rb +118 -0
  92. data/lib/origami/parsers/ppklite.rb +57 -0
  93. data/lib/origami/pdf.rb +1108 -0
  94. data/lib/origami/reference.rb +134 -0
  95. data/lib/origami/signature.rb +702 -0
  96. data/lib/origami/stream.rb +705 -0
  97. data/lib/origami/string.rb +444 -0
  98. data/lib/origami/template/patterns.rb +56 -0
  99. data/lib/origami/template/widgets.rb +151 -0
  100. data/lib/origami/trailer.rb +190 -0
  101. data/lib/origami/tree.rb +62 -0
  102. data/lib/origami/version.rb +23 -0
  103. data/lib/origami/webcapture.rb +100 -0
  104. data/lib/origami/xfa/config.rb +453 -0
  105. data/lib/origami/xfa/connectionset.rb +146 -0
  106. data/lib/origami/xfa/datasets.rb +49 -0
  107. data/lib/origami/xfa/localeset.rb +42 -0
  108. data/lib/origami/xfa/package.rb +59 -0
  109. data/lib/origami/xfa/pdf.rb +73 -0
  110. data/lib/origami/xfa/signature.rb +42 -0
  111. data/lib/origami/xfa/sourceset.rb +43 -0
  112. data/lib/origami/xfa/stylesheet.rb +44 -0
  113. data/lib/origami/xfa/template.rb +1691 -0
  114. data/lib/origami/xfa/xdc.rb +42 -0
  115. data/lib/origami/xfa/xfa.rb +146 -0
  116. data/lib/origami/xfa/xfdf.rb +43 -0
  117. data/lib/origami/xfa/xmpmeta.rb +43 -0
  118. data/lib/origami/xfa.rb +62 -0
  119. data/lib/origami/xreftable.rb +557 -0
  120. data/lib/origami.rb +47 -0
  121. data/test/dataset/calc.pdf +85 -0
  122. data/test/dataset/crypto.pdf +36 -0
  123. data/test/dataset/empty.pdf +49 -0
  124. data/test/test_actions.rb +27 -0
  125. data/test/test_annotations.rb +68 -0
  126. data/test/test_forms.rb +30 -0
  127. data/test/test_native_types.rb +83 -0
  128. data/test/test_object_tree.rb +33 -0
  129. data/test/test_pages.rb +60 -0
  130. data/test/test_pdf.rb +20 -0
  131. data/test/test_pdf_attachment.rb +34 -0
  132. data/test/test_pdf_create.rb +24 -0
  133. data/test/test_pdf_encrypt.rb +102 -0
  134. data/test/test_pdf_parse.rb +134 -0
  135. data/test/test_pdf_parse_lazy.rb +69 -0
  136. data/test/test_pdf_sign.rb +97 -0
  137. data/test/test_streams.rb +184 -0
  138. data/test/test_xrefs.rb +67 -0
  139. metadata +280 -0
data/bin/pdfcop ADDED
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ =begin
4
+
5
+ = Info
6
+ This is a PDF document filtering engine using Origami.
7
+ Security policies are based on a white list of PDF features.
8
+ Default policies details can be found in the default configuration file.
9
+ You can also add your own policy and activate it using the -p switch.
10
+
11
+ = License
12
+ Copyright (C) 2016 Guillaume Delugré.
13
+
14
+ Origami is free software: you can redistribute it and/or modify
15
+ it under the terms of the GNU Lesser General Public License as published by
16
+ the Free Software Foundation, either version 3 of the License, or
17
+ (at your option) any later version.
18
+
19
+ Origami is distributed in the hope that it will be useful,
20
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
+ GNU Lesser General Public License for more details.
23
+
24
+ You should have received a copy of the GNU Lesser General Public License
25
+ along with Origami. If not, see <http://www.gnu.org/licenses/>.
26
+
27
+ =end
28
+
29
+ begin
30
+ require 'origami'
31
+ rescue LoadError
32
+ $: << File.join(__dir__, '../lib')
33
+ require 'origami'
34
+ end
35
+
36
+ require 'optparse'
37
+ require 'yaml'
38
+ require 'rexml/document'
39
+ require 'digest/sha2'
40
+ require 'fileutils'
41
+ require 'rainbow'
42
+
43
+ DEFAULT_CONFIG_FILE = "#{File.dirname(__FILE__)}/config/pdfcop.conf.yml"
44
+ DEFAULT_POLICY = "standard"
45
+ SECURITY_POLICIES = {}
46
+ ANNOTATION_RIGHTS = {
47
+ FileAttachment: %i[allowAttachments allowFileAttachmentAnnotation],
48
+ Sound: %i[allowSoundAnnotation],
49
+ Movie: %i[allowMovieAnnotation],
50
+ Screen: %i[allowScreenAnnotation],
51
+ Widget: %i[allowAcroforms],
52
+ RichMedia: %i[allowRichMediaAnnotation],
53
+ :"3D" => %i[allow3DAnnotation]
54
+ }
55
+
56
+
57
+ def load_config_file(path)
58
+ SECURITY_POLICIES.update(Hash.new(false).update YAML.load(File.read(path)))
59
+ end
60
+
61
+ class OptParser
62
+ BANNER = <<USAGE
63
+ Usage: #{$0} [options] <PDF-file>
64
+ The PDF filtering engine. Scans PDF documents for malicious structures.
65
+ Bug reports or feature requests at: http://github.com/gdelugre/origami
66
+
67
+ Options:
68
+ USAGE
69
+
70
+ def self.parse(args)
71
+ options = {colors_enabled: true, password: ''}
72
+
73
+ parser = OptionParser.new do |opts|
74
+ opts.banner = BANNER
75
+
76
+ opts.on("-o", "--output LOG_FILE", "Output log file (default STDOUT)") do |o|
77
+ options[:output_log] = o
78
+ end
79
+
80
+ opts.on("-c", "--config CONFIG_FILE", "Load security policies from given configuration file") do |cf|
81
+ options[:config_file] = cf
82
+ end
83
+
84
+ opts.on("-p", "--policy POLICY_NAME", "Specify applied policy. Predefined policies: 'none', 'standard', 'strong', 'paranoid'") do |policy|
85
+ options[:policy] = policy
86
+ end
87
+
88
+ opts.on("-m", "--move PATH", "Move rejected documents to the specified directory.") do |dir|
89
+ options[:move_dir] = dir
90
+ end
91
+
92
+ opts.on("-P", "--password PASSWORD", "Password to use if the document is encrypted") do |passwd|
93
+ options[:password] = passwd
94
+ end
95
+
96
+ opts.on("-n", "--no-color", "Turn off colorized output") do
97
+ options[:colors_enabled] = false
98
+ end
99
+
100
+ opts.on_tail("-h", "--help", "Show this message") do
101
+ puts opts
102
+ exit
103
+ end
104
+ end
105
+
106
+ parser.parse!(args)
107
+
108
+ options
109
+ end
110
+ end
111
+
112
+ @options = OptParser.parse(ARGV)
113
+ if @options.key?(:output_log)
114
+ LOGGER = File.open(@options[:output_log], "a+")
115
+ else
116
+ LOGGER = STDOUT
117
+ end
118
+
119
+ if not @options.key?(:policy)
120
+ @options[:policy] = DEFAULT_POLICY
121
+ end
122
+
123
+ if @options.key?(:move_dir) and not File.directory?(@options[:move_dir])
124
+ abort "Error: #{@options[:move_dir]} is not a valid directory."
125
+ end
126
+
127
+ Rainbow.enabled = @options[:colors_enabled]
128
+
129
+ load_config_file(@options[:config_file] || DEFAULT_CONFIG_FILE)
130
+ unless SECURITY_POLICIES.key?("POLICY_#{@options[:policy].upcase}")
131
+ abort "Undeclared policy `#{@options[:policy]}'"
132
+ end
133
+
134
+ if ARGV.empty?
135
+ abort "Error: No filename was specified. #{$0} --help for details."
136
+ else
137
+ TARGET = ARGV.shift
138
+ end
139
+
140
+ def log(message, color = :default)
141
+ time_txt = Rainbow("[#{Time.now}]").cyan
142
+ msg_txt = Rainbow(message).color(color)
143
+ LOGGER.puts("#{time_txt} #{msg_txt}")
144
+ end
145
+
146
+ def reject(cause)
147
+ log("Document rejected by policy `#{@options[:policy]}', caused by #{cause.inspect}.", :red)
148
+
149
+ if @options.key?(:move_dir)
150
+ quarantine(TARGET, @options[:move_dir])
151
+ end
152
+
153
+ abort
154
+ end
155
+
156
+ def quarantine(file, quarantine_folder)
157
+ digest = Digest::SHA256.file(file)
158
+ ext = File.extname(file)
159
+ dest_name = "#{File.basename(file, ext)}_#{digest}#{ext}"
160
+ dest_path = File.join(quarantine_folder, dest_name)
161
+
162
+ FileUtils.move(file, dest_path)
163
+ end
164
+
165
+ def check_rights(*required_rights)
166
+ current_rights = SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]
167
+
168
+ reject(required_rights) if required_rights.any?{|right| current_rights[right.to_s] == false}
169
+ end
170
+
171
+ def analyze_xfa_forms(xfa)
172
+ case xfa
173
+ when Origami::Array then
174
+ xml = ""
175
+ i = 0
176
+ xfa.each do |packet|
177
+ if i % 2 == 1
178
+ xml << packet.solve.data
179
+ end
180
+
181
+ i = i + 1
182
+ end
183
+ when Origami::Stream then
184
+ xml = xfa.data
185
+ else
186
+ reject("Malformed XFA dictionary")
187
+ end
188
+
189
+ xfadoc = REXML::Document.new(xml)
190
+ REXML::XPath.match(xfadoc, "//script").each do |script|
191
+ case script.attributes["contentType"]
192
+ when "application/x-formcalc" then
193
+ check_rights(:allowFormCalc)
194
+ else
195
+ check_rights(:allowJS)
196
+ end
197
+ end
198
+ end
199
+
200
+ def check_annotation_rights(annot)
201
+ subtype = annot.Subtype.value
202
+
203
+ check_rights(*ANNOTATION_RIGHTS[subtype]) if ANNOTATION_RIGHTS.include?(subtype)
204
+ end
205
+
206
+ def analyze_annotation(annot, _level = 0)
207
+ check_rights(:allowAnnotations)
208
+
209
+ if annot.is_a?(Origami::Dictionary) and annot.key?(:Subtype)
210
+ check_annotation_rights(annot)
211
+
212
+ analyze_3d_annotation(annot) if annot.Subtype.value == :"3D"
213
+ end
214
+ end
215
+
216
+ def analyze_3d_annotation(annot)
217
+ # 3D annotation might pull in JavaScript for real-time driven behavior.
218
+ return unless annot.key?(:"3DD")
219
+
220
+ dd = annot[:"3DD"].solve
221
+ u3dstream = nil
222
+
223
+ case dd
224
+ when Origami::Stream
225
+ u3dstream = dd
226
+ when Origami::Dictionary
227
+ u3dstream = dd[:"3DD"].solve
228
+ end
229
+
230
+ if u3dstream.is_a?(Stream) and u3dstream.key?(:OnInstantiate)
231
+ check_rights(:allowJS)
232
+
233
+ if annot.key?(:"3DA") # is 3d view instantiated automatically?
234
+ u3dactiv = annot[:"3DA"].solve
235
+
236
+ check_rights(:allowJSAtOpening) if u3dactiv.is_a?(Origami::Dictionary) and (u3dactiv.A == :PO or u3dactiv.A == :PV)
237
+ end
238
+ end
239
+ end
240
+
241
+ def analyze_page(page, level = 0)
242
+ section_prefix = " " * 2 * level + ">" * (level + 1)
243
+ log(section_prefix + " Inspecting page...")
244
+
245
+ text_prefix = " " * 2 * (level + 1) + "." * (level + 1)
246
+ if page.is_a?(Origami::Dictionary)
247
+ #
248
+ # Checking page additional actions.
249
+ #
250
+ if page.key?(:AA)
251
+ if page.AA.is_a?(Origami::Dictionary)
252
+ log(text_prefix + " Page has an action dictionary.")
253
+
254
+ aa = Origami::Page::AdditionalActions.new(page.AA); aa.parent = page.AA.parent
255
+ analyze_action(aa.O, true, level + 1) if aa.key?(:O)
256
+ analyze_action(aa.C, false, level + 1) if aa.key?(:C)
257
+ end
258
+ end
259
+
260
+ #
261
+ # Looking for page annotations.
262
+ #
263
+ page.each_annotation do |annot|
264
+ analyze_annotation(annot, level + 1)
265
+ end
266
+ end
267
+ end
268
+
269
+ def analyze_action(action, triggered_at_opening, level = 0)
270
+ section_prefix = " " * 2 * level + ">" * (level + 1)
271
+ log(section_prefix + " Inspecting action...")
272
+
273
+ text_prefix = " " * 2 * (level + 1) + "." * (level + 1)
274
+ if action.is_a?(Origami::Dictionary)
275
+ log(text_prefix + " Found #{action[:S]} action.")
276
+ type = action[:S].is_a?(Origami::Reference) ? action[:S].solve : action[:S]
277
+
278
+ case type.value
279
+ when :JavaScript
280
+ check_rights(:allowJS)
281
+ check_rights(:allowJSAtOpening) if triggered_at_opening
282
+
283
+ when :Launch
284
+ check_rights(:allowLaunchAction)
285
+
286
+ when :Named
287
+ check_rights(:allowNamedAction)
288
+
289
+ when :GoTo
290
+ check_rights(:allowGoToAction)
291
+ dest = action[:D].is_a?(Origami::Reference) ? action[:D].solve : action[:D]
292
+ if dest.is_a?(Origami::Array) and dest.length > 0 and dest.first.is_a?(Origami::Reference)
293
+ dest_page = dest.first.solve
294
+ if dest_page.is_a?(Origami::Page)
295
+ log(text_prefix + " Destination page found.")
296
+ analyze_page(dest_page, level + 1)
297
+ end
298
+ end
299
+
300
+ when :GoToE
301
+ check_rights(:allowAttachments,:allowGoToEAction)
302
+
303
+ when :GoToR
304
+ check_rights(:allowGoToRAction)
305
+
306
+ when :Thread
307
+ check_rights(:allowGoToRAction) if action.key?(:F)
308
+
309
+ when :URI
310
+ check_rights(:allowURIAction)
311
+
312
+ when :SubmitForm
313
+ check_rights(:allowAcroForms,:allowSubmitFormAction)
314
+
315
+ when :ImportData
316
+ check_rights(:allowAcroForms,:allowImportDataAction)
317
+
318
+ when :Rendition
319
+ check_rights(:allowScreenAnnotation,:allowRenditionAction)
320
+
321
+ when :Sound
322
+ check_rights(:allowSoundAnnotation,:allowSoundAction)
323
+
324
+ when :Movie
325
+ check_rights(:allowMovieAnnotation,:allowMovieAction)
326
+
327
+ when :RichMediaExecute
328
+ check_rights(:allowRichMediaAnnotation,:allowRichMediaAction)
329
+
330
+ when :GoTo3DView
331
+ check_rights(:allow3DAnnotation,:allowGoTo3DAction)
332
+ end
333
+
334
+ if action.key?(:Next)
335
+ log(text_prefix + "This action is chained to another action!")
336
+ check_rights(:allowChainedActions)
337
+ analyze_action(action.Next)
338
+ end
339
+
340
+ elsif action.is_a?(Origami::Array)
341
+ dest = action
342
+ if dest.length > 0 and dest.first.is_a?(Origami::Reference)
343
+ dest_page = dest.first.solve
344
+ if dest_page.is_a?(Origami::Page)
345
+ log(text_prefix + " Destination page found.")
346
+ check_rights(:allowGoToAction)
347
+ analyze_page(dest_page, level + 1)
348
+ end
349
+ end
350
+ end
351
+ end
352
+
353
+ begin
354
+ log("PDFcop is running on target `#{TARGET}', policy = `#{@options[:policy]}'", :green)
355
+ log(" File size: #{File.size(TARGET)} bytes", :magenta)
356
+ log(" SHA256: #{Digest::SHA256.file(TARGET)}", :magenta)
357
+
358
+ @pdf = Origami::PDF.read(TARGET,
359
+ verbosity: Origami::Parser::VERBOSE_QUIET,
360
+ ignore_errors: SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]['allowParserErrors'],
361
+ decrypt: SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]['allowEncryption'],
362
+ prompt_password: lambda { '' },
363
+ password: @options[:password]
364
+ )
365
+
366
+ log("> Inspecting document structure...", :yellow)
367
+ if @pdf.encrypted?
368
+ log(" . Encryption = YES")
369
+ check_rights(:allowEncryption)
370
+ end
371
+
372
+ log("> Inspecting document catalog...", :yellow)
373
+ catalog = @pdf.Catalog
374
+ reject("Invalid document catalog") unless catalog.is_a?(Origami::Catalog)
375
+
376
+ if catalog.key?(:OpenAction)
377
+ log(" . OpenAction entry = YES")
378
+ check_rights(:allowOpenAction)
379
+ action = catalog.OpenAction
380
+ analyze_action(action, true, 1)
381
+ end
382
+
383
+ if catalog.key?(:AA)
384
+ if catalog.AA.is_a?(Origami::Dictionary)
385
+ aa = Origami::CatalogAdditionalActions.new(catalog.AA); aa.parent = catalog;
386
+ log(" . Additional actions dictionary = YES")
387
+ analyze_action(aa.WC, false, 1) if aa.key?(:WC)
388
+ analyze_action(aa.WS, false, 1) if aa.key?(:WS)
389
+ analyze_action(aa.DS, false, 1) if aa.key?(:DS)
390
+ analyze_action(aa.WP, false, 1) if aa.key?(:WP)
391
+ analyze_action(aa.DP, false, 1) if aa.key?(:DP)
392
+ end
393
+ end
394
+
395
+ if catalog.key?(:AcroForm)
396
+ acroform = catalog.AcroForm
397
+ if acroform.is_a?(Origami::Dictionary)
398
+ log(" . AcroForm = YES")
399
+ check_rights(:allowAcroForms)
400
+ if acroform.key?(:XFA)
401
+ log(" . XFA = YES")
402
+ check_rights(:allowXFAForms)
403
+
404
+ analyze_xfa_forms(acroform[:XFA].solve)
405
+ end
406
+ end
407
+ end
408
+
409
+ log("> Inspecting JavaScript names directory...", :yellow)
410
+ if @pdf.each_named_script.any?
411
+ check_rights(:allowJS)
412
+ check_rights(:allowJSAtOpening)
413
+ end
414
+
415
+ log("> Inspecting attachment names directory...", :yellow)
416
+ if @pdf.each_attachment.any?
417
+ check_rights(:allowAttachments)
418
+ end
419
+
420
+ log("> Inspecting document pages...", :yellow)
421
+ @pdf.each_page do |page|
422
+ analyze_page(page, 1)
423
+ end
424
+
425
+ log("> Inspecting document streams...", :yellow)
426
+ @pdf.each_object.select{|obj| obj.is_a?(Origami::Stream)}.each do |stream|
427
+ if stream.dictionary.key?(:Filter)
428
+ filters = stream.Filter
429
+ filters = [ filters ] if filters.is_a?(Origami::Name)
430
+
431
+ if filters.is_a?(Origami::Array)
432
+ filters.each do |filter|
433
+ case filter.value
434
+ when :ASCIIHexDecode
435
+ check_rights(:allowASCIIHexFilter)
436
+ when :ASCII85Decode
437
+ check_rights(:allowASCII85Filter)
438
+ when :LZWDecode
439
+ check_rights(:allowLZWFilter)
440
+ when :FlateDecode
441
+ check_rights(:allowFlateDecode)
442
+ when :RunLengthDecode
443
+ check_rights(:allowRunLengthFilter)
444
+ when :CCITTFaxDecode
445
+ check_rights(:allowCCITTFaxFilter)
446
+ when :JBIG2Decode
447
+ check_rights(:allowJBIG2Filter)
448
+ when :DCTDecode
449
+ check_rights(:allowDCTFilter)
450
+ when :JPXDecode
451
+ check_rights(:allowJPXFilter)
452
+ when :Crypt
453
+ check_rights(:allowCryptFilter)
454
+ end
455
+ end
456
+ end
457
+ end
458
+ end
459
+
460
+ #
461
+ # TODO: Detect JS at opening in XFA (check event tag)
462
+ # Check image encoding in XFA ?
463
+ # Only allow valid signed documents ?
464
+ # Recursively scan attached files.
465
+ # On-the-fly injection of prerun JS code to hook vulnerable methods (dynamic exploit detection) ???
466
+ # ...
467
+ #
468
+
469
+ log("Document accepted by policy `#{@options[:policy]}'.", :green)
470
+
471
+ rescue
472
+ log("An error occurred during analysis: #{$!.class} (#{$!.message})")
473
+ reject("Analysis failure")
474
+ ensure
475
+ LOGGER.close
476
+ end
data/bin/pdfdecompress ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ =begin
4
+
5
+ = Info
6
+ Uncompresses all binary streams of a PDF document.
7
+
8
+ = License
9
+ Copyright (C) 2016 Guillaume Delugré.
10
+
11
+ Origami is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU Lesser General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
15
+
16
+ Origami is distributed in the hope that it will be useful,
17
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ GNU Lesser General Public License for more details.
20
+
21
+ You should have received a copy of the GNU Lesser General Public License
22
+ along with Origami. If not, see <http://www.gnu.org/licenses/>.
23
+
24
+ =end
25
+
26
+ begin
27
+ require 'origami'
28
+ rescue LoadError
29
+ $: << File.join(__dir__, '../lib')
30
+ require 'origami'
31
+ end
32
+ include Origami
33
+
34
+ require 'optparse'
35
+
36
+ class OptParser
37
+ BANNER = <<USAGE
38
+ Usage: #{$0} [<PDF-file>] [-p <password>] [-o <output-file>]
39
+ Uncompresses all binary streams of a PDF document.
40
+ Bug reports or feature requests at: http://github.com/gdelugre/origami
41
+
42
+ Options:
43
+ USAGE
44
+
45
+ def self.parser(options)
46
+ OptionParser.new do |opts|
47
+ opts.banner = BANNER
48
+
49
+ opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
50
+ options[:output] = o
51
+ end
52
+
53
+ opts.on_tail("-h", "--help", "Show this message") do
54
+ puts opts
55
+ exit
56
+ end
57
+ end
58
+ end
59
+
60
+ def self.parse(args)
61
+ options =
62
+ {
63
+ output: STDOUT,
64
+ }
65
+
66
+ self.parser(options).parse!(args)
67
+
68
+ options
69
+ end
70
+ end
71
+
72
+ begin
73
+ @options = OptParser.parse(ARGV)
74
+
75
+ target = (ARGV.empty?) ? STDIN : ARGV.shift
76
+ params =
77
+ {
78
+ verbosity: Parser::VERBOSE_QUIET,
79
+ }
80
+
81
+ pdf = PDF.read(target, params)
82
+
83
+ pdf.each_object
84
+ .select { |obj| obj.is_a?(Stream) }
85
+ .each { |stream|
86
+ unless stream.filters.any?{|filter| %i[JPXDecode DCTDecode JBIG2Decode].include?(filter.value) }
87
+ stream.encoded_data = stream.data
88
+ stream.dictionary.delete(:Filter)
89
+ end
90
+ }
91
+
92
+ pdf.save(@options[:output], noindent: true)
93
+
94
+ rescue
95
+ STDERR.puts $!.backtrace.join($/)
96
+ abort "#{$!.class}: #{$!.message}"
97
+ end
data/bin/pdfdecrypt ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ =begin
4
+
5
+ = Info
6
+ Decrypts a PDF document.
7
+
8
+ = License
9
+ Copyright (C) 2016 Guillaume Delugré.
10
+
11
+ Origami is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU Lesser General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
15
+
16
+ Origami is distributed in the hope that it will be useful,
17
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ GNU Lesser General Public License for more details.
20
+
21
+ You should have received a copy of the GNU Lesser General Public License
22
+ along with Origami. If not, see <http://www.gnu.org/licenses/>.
23
+
24
+ =end
25
+
26
+ begin
27
+ require 'origami'
28
+ rescue LoadError
29
+ $: << File.join(__dir__, '../lib')
30
+ require 'origami'
31
+ end
32
+ include Origami
33
+
34
+ require 'optparse'
35
+
36
+ class OptParser
37
+ BANNER = <<USAGE
38
+ Usage: #{$0} [<PDF-file>] [-p <password>] [-o <output-file>]
39
+ Decrypts a PDF document. Supports RC4 40 to 128 bits, AES128, AES256.
40
+ Bug reports or feature requests at: http://github.com/gdelugre/origami
41
+
42
+ Options:
43
+ USAGE
44
+
45
+ def self.parser(options)
46
+ OptionParser.new do |opts|
47
+ opts.banner = BANNER
48
+
49
+ opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
50
+ options[:output] = o
51
+ end
52
+
53
+ opts.on("-p", "--password PASSWORD", "Password of the document") do |p|
54
+ options[:password] = p
55
+ end
56
+
57
+ opts.on_tail("-h", "--help", "Show this message") do
58
+ puts opts
59
+ exit
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.parse(args)
65
+ options =
66
+ {
67
+ output: STDOUT,
68
+ password: ''
69
+ }
70
+
71
+ self.parser(options).parse!(args)
72
+
73
+ options
74
+ end
75
+ end
76
+
77
+ begin
78
+ @options = OptParser.parse(ARGV)
79
+
80
+ target = (ARGV.empty?) ? STDIN : ARGV.shift
81
+ params =
82
+ {
83
+ verbosity: Parser::VERBOSE_QUIET,
84
+ password: @options[:password]
85
+ }
86
+
87
+ PDF.read(target, params).save(@options[:output], decrypt: true, noindent: true)
88
+
89
+ rescue
90
+ abort "#{$!.class}: #{$!.message}"
91
+ end