hexapdf 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CONTRIBUTERS +3 -0
- data/LICENSE +26 -0
- data/README.md +88 -0
- data/Rakefile +121 -0
- data/VERSION +1 -0
- data/agpl-3.0.txt +661 -0
- data/bin/hexapdf +6 -0
- data/data/hexapdf/afm/Courier-Bold.afm +342 -0
- data/data/hexapdf/afm/Courier-BoldOblique.afm +342 -0
- data/data/hexapdf/afm/Courier-Oblique.afm +342 -0
- data/data/hexapdf/afm/Courier.afm +342 -0
- data/data/hexapdf/afm/Helvetica-Bold.afm +2827 -0
- data/data/hexapdf/afm/Helvetica-BoldOblique.afm +2827 -0
- data/data/hexapdf/afm/Helvetica-Oblique.afm +3051 -0
- data/data/hexapdf/afm/Helvetica.afm +3051 -0
- data/data/hexapdf/afm/MustRead.html +1 -0
- data/data/hexapdf/afm/Symbol.afm +213 -0
- data/data/hexapdf/afm/Times-Bold.afm +2588 -0
- data/data/hexapdf/afm/Times-BoldItalic.afm +2384 -0
- data/data/hexapdf/afm/Times-Italic.afm +2667 -0
- data/data/hexapdf/afm/Times-Roman.afm +2419 -0
- data/data/hexapdf/afm/ZapfDingbats.afm +225 -0
- data/data/hexapdf/encoding/glyphlist.txt +4305 -0
- data/data/hexapdf/encoding/zapfdingbats.txt +225 -0
- data/examples/arc.rb +50 -0
- data/examples/graphics.rb +274 -0
- data/examples/hello_world.rb +16 -0
- data/examples/machupicchu.jpg +0 -0
- data/examples/merging.rb +24 -0
- data/examples/optimizing.rb +20 -0
- data/examples/show_char_bboxes.rb +55 -0
- data/examples/standard_pdf_fonts.rb +72 -0
- data/examples/truetype.rb +45 -0
- data/lib/hexapdf/cli/extract.rb +128 -0
- data/lib/hexapdf/cli/info.rb +121 -0
- data/lib/hexapdf/cli/inspect.rb +157 -0
- data/lib/hexapdf/cli/modify.rb +218 -0
- data/lib/hexapdf/cli.rb +121 -0
- data/lib/hexapdf/configuration.rb +392 -0
- data/lib/hexapdf/content/canvas.rb +1974 -0
- data/lib/hexapdf/content/color_space.rb +364 -0
- data/lib/hexapdf/content/graphic_object/arc.rb +267 -0
- data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +208 -0
- data/lib/hexapdf/content/graphic_object/solid_arc.rb +173 -0
- data/lib/hexapdf/content/graphic_object.rb +81 -0
- data/lib/hexapdf/content/graphics_state.rb +579 -0
- data/lib/hexapdf/content/operator.rb +1072 -0
- data/lib/hexapdf/content/parser.rb +204 -0
- data/lib/hexapdf/content/processor.rb +451 -0
- data/lib/hexapdf/content/transformation_matrix.rb +172 -0
- data/lib/hexapdf/content.rb +47 -0
- data/lib/hexapdf/data_dir.rb +51 -0
- data/lib/hexapdf/dictionary.rb +303 -0
- data/lib/hexapdf/dictionary_fields.rb +382 -0
- data/lib/hexapdf/document.rb +589 -0
- data/lib/hexapdf/document_utils.rb +209 -0
- data/lib/hexapdf/encryption/aes.rb +206 -0
- data/lib/hexapdf/encryption/arc4.rb +93 -0
- data/lib/hexapdf/encryption/fast_aes.rb +79 -0
- data/lib/hexapdf/encryption/fast_arc4.rb +67 -0
- data/lib/hexapdf/encryption/identity.rb +63 -0
- data/lib/hexapdf/encryption/ruby_aes.rb +447 -0
- data/lib/hexapdf/encryption/ruby_arc4.rb +96 -0
- data/lib/hexapdf/encryption/security_handler.rb +494 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +616 -0
- data/lib/hexapdf/encryption.rb +94 -0
- data/lib/hexapdf/error.rb +73 -0
- data/lib/hexapdf/filter/ascii85_decode.rb +160 -0
- data/lib/hexapdf/filter/ascii_hex_decode.rb +87 -0
- data/lib/hexapdf/filter/dct_decode.rb +57 -0
- data/lib/hexapdf/filter/encryption.rb +59 -0
- data/lib/hexapdf/filter/flate_decode.rb +93 -0
- data/lib/hexapdf/filter/jpx_decode.rb +56 -0
- data/lib/hexapdf/filter/lzw_decode.rb +191 -0
- data/lib/hexapdf/filter/predictor.rb +266 -0
- data/lib/hexapdf/filter/run_length_decode.rb +108 -0
- data/lib/hexapdf/filter.rb +176 -0
- data/lib/hexapdf/font/cmap/parser.rb +146 -0
- data/lib/hexapdf/font/cmap/writer.rb +176 -0
- data/lib/hexapdf/font/cmap.rb +90 -0
- data/lib/hexapdf/font/encoding/base.rb +77 -0
- data/lib/hexapdf/font/encoding/difference_encoding.rb +64 -0
- data/lib/hexapdf/font/encoding/glyph_list.rb +150 -0
- data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +221 -0
- data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +265 -0
- data/lib/hexapdf/font/encoding/standard_encoding.rb +205 -0
- data/lib/hexapdf/font/encoding/symbol_encoding.rb +244 -0
- data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +280 -0
- data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +250 -0
- data/lib/hexapdf/font/encoding.rb +68 -0
- data/lib/hexapdf/font/true_type/font.rb +179 -0
- data/lib/hexapdf/font/true_type/table/cmap.rb +103 -0
- data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +384 -0
- data/lib/hexapdf/font/true_type/table/directory.rb +92 -0
- data/lib/hexapdf/font/true_type/table/glyf.rb +166 -0
- data/lib/hexapdf/font/true_type/table/head.rb +143 -0
- data/lib/hexapdf/font/true_type/table/hhea.rb +109 -0
- data/lib/hexapdf/font/true_type/table/hmtx.rb +79 -0
- data/lib/hexapdf/font/true_type/table/loca.rb +79 -0
- data/lib/hexapdf/font/true_type/table/maxp.rb +112 -0
- data/lib/hexapdf/font/true_type/table/name.rb +218 -0
- data/lib/hexapdf/font/true_type/table/os2.rb +200 -0
- data/lib/hexapdf/font/true_type/table/post.rb +230 -0
- data/lib/hexapdf/font/true_type/table.rb +155 -0
- data/lib/hexapdf/font/true_type.rb +48 -0
- data/lib/hexapdf/font/true_type_wrapper.rb +240 -0
- data/lib/hexapdf/font/type1/afm_parser.rb +230 -0
- data/lib/hexapdf/font/type1/character_metrics.rb +67 -0
- data/lib/hexapdf/font/type1/font.rb +123 -0
- data/lib/hexapdf/font/type1/font_metrics.rb +117 -0
- data/lib/hexapdf/font/type1/pfb_parser.rb +71 -0
- data/lib/hexapdf/font/type1.rb +52 -0
- data/lib/hexapdf/font/type1_wrapper.rb +193 -0
- data/lib/hexapdf/font_loader/from_configuration.rb +70 -0
- data/lib/hexapdf/font_loader/standard14.rb +98 -0
- data/lib/hexapdf/font_loader.rb +85 -0
- data/lib/hexapdf/font_utils.rb +89 -0
- data/lib/hexapdf/image_loader/jpeg.rb +166 -0
- data/lib/hexapdf/image_loader/pdf.rb +89 -0
- data/lib/hexapdf/image_loader/png.rb +410 -0
- data/lib/hexapdf/image_loader.rb +68 -0
- data/lib/hexapdf/importer.rb +139 -0
- data/lib/hexapdf/name_tree_node.rb +78 -0
- data/lib/hexapdf/number_tree_node.rb +67 -0
- data/lib/hexapdf/object.rb +363 -0
- data/lib/hexapdf/parser.rb +349 -0
- data/lib/hexapdf/rectangle.rb +99 -0
- data/lib/hexapdf/reference.rb +98 -0
- data/lib/hexapdf/revision.rb +206 -0
- data/lib/hexapdf/revisions.rb +194 -0
- data/lib/hexapdf/serializer.rb +326 -0
- data/lib/hexapdf/stream.rb +279 -0
- data/lib/hexapdf/task/dereference.rb +109 -0
- data/lib/hexapdf/task/optimize.rb +230 -0
- data/lib/hexapdf/task.rb +68 -0
- data/lib/hexapdf/tokenizer.rb +406 -0
- data/lib/hexapdf/type/catalog.rb +107 -0
- data/lib/hexapdf/type/embedded_file.rb +87 -0
- data/lib/hexapdf/type/file_specification.rb +232 -0
- data/lib/hexapdf/type/font.rb +81 -0
- data/lib/hexapdf/type/font_descriptor.rb +109 -0
- data/lib/hexapdf/type/font_simple.rb +190 -0
- data/lib/hexapdf/type/font_true_type.rb +47 -0
- data/lib/hexapdf/type/font_type1.rb +162 -0
- data/lib/hexapdf/type/form.rb +103 -0
- data/lib/hexapdf/type/graphics_state_parameter.rb +79 -0
- data/lib/hexapdf/type/image.rb +73 -0
- data/lib/hexapdf/type/info.rb +70 -0
- data/lib/hexapdf/type/names.rb +69 -0
- data/lib/hexapdf/type/object_stream.rb +224 -0
- data/lib/hexapdf/type/page.rb +355 -0
- data/lib/hexapdf/type/page_tree_node.rb +269 -0
- data/lib/hexapdf/type/resources.rb +212 -0
- data/lib/hexapdf/type/trailer.rb +128 -0
- data/lib/hexapdf/type/viewer_preferences.rb +73 -0
- data/lib/hexapdf/type/xref_stream.rb +204 -0
- data/lib/hexapdf/type.rb +67 -0
- data/lib/hexapdf/utils/bit_field.rb +87 -0
- data/lib/hexapdf/utils/bit_stream.rb +148 -0
- data/lib/hexapdf/utils/lru_cache.rb +65 -0
- data/lib/hexapdf/utils/math_helpers.rb +55 -0
- data/lib/hexapdf/utils/object_hash.rb +130 -0
- data/lib/hexapdf/utils/pdf_doc_encoding.rb +93 -0
- data/lib/hexapdf/utils/sorted_tree_node.rb +339 -0
- data/lib/hexapdf/version.rb +39 -0
- data/lib/hexapdf/writer.rb +199 -0
- data/lib/hexapdf/xref_section.rb +152 -0
- data/lib/hexapdf.rb +34 -0
- data/man/man1/hexapdf.1 +249 -0
- data/test/data/aes-test-vectors/CBCGFSbox-128-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCGFSbox-128-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCGFSbox-192-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCGFSbox-192-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCGFSbox-256-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCGFSbox-256-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCKeySbox-128-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCKeySbox-128-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCKeySbox-192-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCKeySbox-192-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCKeySbox-256-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCKeySbox-256-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarKey-128-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarKey-128-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarKey-192-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarKey-192-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarKey-256-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarKey-256-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarTxt-128-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarTxt-128-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarTxt-192-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarTxt-192-encrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarTxt-256-decrypt.data.gz +0 -0
- data/test/data/aes-test-vectors/CBCVarTxt-256-encrypt.data.gz +0 -0
- data/test/data/fonts/Ubuntu-Title.ttf +0 -0
- data/test/data/images/cmyk.jpg +0 -0
- data/test/data/images/fillbytes.jpg +0 -0
- data/test/data/images/gray.jpg +0 -0
- data/test/data/images/greyscale-1bit.png +0 -0
- data/test/data/images/greyscale-2bit.png +0 -0
- data/test/data/images/greyscale-4bit.png +0 -0
- data/test/data/images/greyscale-8bit.png +0 -0
- data/test/data/images/greyscale-alpha-8bit.png +0 -0
- data/test/data/images/greyscale-trns-8bit.png +0 -0
- data/test/data/images/greyscale-with-gamma1.0.png +0 -0
- data/test/data/images/greyscale-with-gamma1.5.png +0 -0
- data/test/data/images/indexed-1bit.png +0 -0
- data/test/data/images/indexed-2bit.png +0 -0
- data/test/data/images/indexed-4bit.png +0 -0
- data/test/data/images/indexed-8bit.png +0 -0
- data/test/data/images/indexed-alpha-4bit.png +0 -0
- data/test/data/images/indexed-alpha-8bit.png +0 -0
- data/test/data/images/rgb.jpg +0 -0
- data/test/data/images/truecolour-8bit.png +0 -0
- data/test/data/images/truecolour-alpha-8bit.png +0 -0
- data/test/data/images/truecolour-gama-chrm-8bit.png +0 -0
- data/test/data/images/truecolour-srgb-8bit.png +0 -0
- data/test/data/minimal.pdf +44 -0
- data/test/data/standard-security-handler/README +9 -0
- data/test/data/standard-security-handler/bothpwd-aes-128bit-V4.pdf +44 -0
- data/test/data/standard-security-handler/bothpwd-aes-256bit-V5.pdf +0 -0
- data/test/data/standard-security-handler/bothpwd-arc4-128bit-V2.pdf +43 -0
- data/test/data/standard-security-handler/bothpwd-arc4-128bit-V4.pdf +43 -0
- data/test/data/standard-security-handler/bothpwd-arc4-40bit-V1.pdf +0 -0
- data/test/data/standard-security-handler/nopwd-aes-128bit-V4.pdf +43 -0
- data/test/data/standard-security-handler/nopwd-aes-256bit-V5.pdf +0 -0
- data/test/data/standard-security-handler/nopwd-arc4-128bit-V2.pdf +43 -0
- data/test/data/standard-security-handler/nopwd-arc4-128bit-V4.pdf +43 -0
- data/test/data/standard-security-handler/nopwd-arc4-40bit-V1.pdf +43 -0
- data/test/data/standard-security-handler/ownerpwd-aes-128bit-V4.pdf +0 -0
- data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5.pdf +43 -0
- data/test/data/standard-security-handler/ownerpwd-arc4-128bit-V2.pdf +43 -0
- data/test/data/standard-security-handler/ownerpwd-arc4-128bit-V4.pdf +43 -0
- data/test/data/standard-security-handler/ownerpwd-arc4-40bit-V1.pdf +43 -0
- data/test/data/standard-security-handler/userpwd-aes-128bit-V4.pdf +43 -0
- data/test/data/standard-security-handler/userpwd-aes-256bit-V5.pdf +43 -0
- data/test/data/standard-security-handler/userpwd-arc4-128bit-V2.pdf +0 -0
- data/test/data/standard-security-handler/userpwd-arc4-128bit-V4.pdf +0 -0
- data/test/data/standard-security-handler/userpwd-arc4-40bit-V1.pdf +43 -0
- data/test/hexapdf/common_tokenizer_tests.rb +204 -0
- data/test/hexapdf/content/common.rb +31 -0
- data/test/hexapdf/content/graphic_object/test_arc.rb +93 -0
- data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +91 -0
- data/test/hexapdf/content/graphic_object/test_solid_arc.rb +86 -0
- data/test/hexapdf/content/test_canvas.rb +1113 -0
- data/test/hexapdf/content/test_color_space.rb +97 -0
- data/test/hexapdf/content/test_graphics_state.rb +138 -0
- data/test/hexapdf/content/test_operator.rb +619 -0
- data/test/hexapdf/content/test_parser.rb +66 -0
- data/test/hexapdf/content/test_processor.rb +156 -0
- data/test/hexapdf/content/test_transformation_matrix.rb +64 -0
- data/test/hexapdf/encryption/common.rb +87 -0
- data/test/hexapdf/encryption/test_aes.rb +121 -0
- data/test/hexapdf/encryption/test_arc4.rb +39 -0
- data/test/hexapdf/encryption/test_fast_aes.rb +17 -0
- data/test/hexapdf/encryption/test_fast_arc4.rb +12 -0
- data/test/hexapdf/encryption/test_identity.rb +21 -0
- data/test/hexapdf/encryption/test_ruby_aes.rb +23 -0
- data/test/hexapdf/encryption/test_ruby_arc4.rb +20 -0
- data/test/hexapdf/encryption/test_security_handler.rb +356 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +274 -0
- data/test/hexapdf/filter/common.rb +53 -0
- data/test/hexapdf/filter/test_ascii85_decode.rb +60 -0
- data/test/hexapdf/filter/test_ascii_hex_decode.rb +33 -0
- data/test/hexapdf/filter/test_encryption.rb +24 -0
- data/test/hexapdf/filter/test_flate_decode.rb +35 -0
- data/test/hexapdf/filter/test_lzw_decode.rb +52 -0
- data/test/hexapdf/filter/test_predictor.rb +183 -0
- data/test/hexapdf/filter/test_run_length_decode.rb +32 -0
- data/test/hexapdf/font/cmap/test_parser.rb +67 -0
- data/test/hexapdf/font/cmap/test_writer.rb +58 -0
- data/test/hexapdf/font/encoding/test_base.rb +35 -0
- data/test/hexapdf/font/encoding/test_difference_encoding.rb +21 -0
- data/test/hexapdf/font/encoding/test_glyph_list.rb +59 -0
- data/test/hexapdf/font/encoding/test_zapf_dingbats_encoding.rb +16 -0
- data/test/hexapdf/font/test_encoding.rb +27 -0
- data/test/hexapdf/font/test_true_type_wrapper.rb +110 -0
- data/test/hexapdf/font/test_type1_wrapper.rb +66 -0
- data/test/hexapdf/font/true_type/common.rb +19 -0
- data/test/hexapdf/font/true_type/table/test_cmap.rb +59 -0
- data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +133 -0
- data/test/hexapdf/font/true_type/table/test_directory.rb +35 -0
- data/test/hexapdf/font/true_type/table/test_glyf.rb +58 -0
- data/test/hexapdf/font/true_type/table/test_head.rb +76 -0
- data/test/hexapdf/font/true_type/table/test_hhea.rb +40 -0
- data/test/hexapdf/font/true_type/table/test_hmtx.rb +38 -0
- data/test/hexapdf/font/true_type/table/test_loca.rb +43 -0
- data/test/hexapdf/font/true_type/table/test_maxp.rb +62 -0
- data/test/hexapdf/font/true_type/table/test_name.rb +95 -0
- data/test/hexapdf/font/true_type/table/test_os2.rb +65 -0
- data/test/hexapdf/font/true_type/table/test_post.rb +89 -0
- data/test/hexapdf/font/true_type/test_font.rb +120 -0
- data/test/hexapdf/font/true_type/test_table.rb +41 -0
- data/test/hexapdf/font/type1/test_afm_parser.rb +51 -0
- data/test/hexapdf/font/type1/test_font.rb +68 -0
- data/test/hexapdf/font/type1/test_pfb_parser.rb +37 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +28 -0
- data/test/hexapdf/font_loader/test_standard14.rb +22 -0
- data/test/hexapdf/image_loader/test_jpeg.rb +83 -0
- data/test/hexapdf/image_loader/test_pdf.rb +47 -0
- data/test/hexapdf/image_loader/test_png.rb +258 -0
- data/test/hexapdf/task/test_dereference.rb +46 -0
- data/test/hexapdf/task/test_optimize.rb +137 -0
- data/test/hexapdf/test_configuration.rb +82 -0
- data/test/hexapdf/test_data_dir.rb +32 -0
- data/test/hexapdf/test_dictionary.rb +284 -0
- data/test/hexapdf/test_dictionary_fields.rb +185 -0
- data/test/hexapdf/test_document.rb +574 -0
- data/test/hexapdf/test_document_utils.rb +144 -0
- data/test/hexapdf/test_filter.rb +96 -0
- data/test/hexapdf/test_font_utils.rb +47 -0
- data/test/hexapdf/test_importer.rb +78 -0
- data/test/hexapdf/test_object.rb +177 -0
- data/test/hexapdf/test_parser.rb +394 -0
- data/test/hexapdf/test_rectangle.rb +36 -0
- data/test/hexapdf/test_reference.rb +41 -0
- data/test/hexapdf/test_revision.rb +139 -0
- data/test/hexapdf/test_revisions.rb +93 -0
- data/test/hexapdf/test_serializer.rb +169 -0
- data/test/hexapdf/test_stream.rb +262 -0
- data/test/hexapdf/test_tokenizer.rb +30 -0
- data/test/hexapdf/test_writer.rb +120 -0
- data/test/hexapdf/test_xref_section.rb +35 -0
- data/test/hexapdf/type/test_catalog.rb +30 -0
- data/test/hexapdf/type/test_embedded_file.rb +16 -0
- data/test/hexapdf/type/test_file_specification.rb +148 -0
- data/test/hexapdf/type/test_font.rb +35 -0
- data/test/hexapdf/type/test_font_descriptor.rb +51 -0
- data/test/hexapdf/type/test_font_simple.rb +190 -0
- data/test/hexapdf/type/test_font_type1.rb +128 -0
- data/test/hexapdf/type/test_form.rb +60 -0
- data/test/hexapdf/type/test_info.rb +14 -0
- data/test/hexapdf/type/test_names.rb +9 -0
- data/test/hexapdf/type/test_object_stream.rb +84 -0
- data/test/hexapdf/type/test_page.rb +260 -0
- data/test/hexapdf/type/test_page_tree_node.rb +255 -0
- data/test/hexapdf/type/test_resources.rb +167 -0
- data/test/hexapdf/type/test_trailer.rb +109 -0
- data/test/hexapdf/type/test_xref_stream.rb +131 -0
- data/test/hexapdf/utils/test_bit_field.rb +47 -0
- data/test/hexapdf/utils/test_lru_cache.rb +22 -0
- data/test/hexapdf/utils/test_object_hash.rb +115 -0
- data/test/hexapdf/utils/test_pdf_doc_encoding.rb +18 -0
- data/test/hexapdf/utils/test_sorted_tree_node.rb +232 -0
- data/test/test_helper.rb +56 -0
- metadata +427 -0
@@ -0,0 +1,616 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# This file is part of HexaPDF.
|
5
|
+
#
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
7
|
+
# Copyright (C) 2016 Thomas Leitner
|
8
|
+
#
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
16
|
+
#
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
20
|
+
# License for more details.
|
21
|
+
#
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
24
|
+
#
|
25
|
+
# The interactive user interfaces in modified source and object code
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
28
|
+
#
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
31
|
+
# is created or manipulated using HexaPDF.
|
32
|
+
#++
|
33
|
+
|
34
|
+
require 'hexapdf/encryption/security_handler'
|
35
|
+
require 'digest/md5'
|
36
|
+
require 'digest/sha2'
|
37
|
+
|
38
|
+
module HexaPDF
|
39
|
+
module Encryption
|
40
|
+
|
41
|
+
# The specialized encryption dictionary for the StandardSecurityHandler.
|
42
|
+
#
|
43
|
+
# Contains additional fields that are used for storing the information needed for retrieving
|
44
|
+
# the encryption key and a set of permissions.
|
45
|
+
class StandardEncryptionDictionary < EncryptionDictionary
|
46
|
+
|
47
|
+
define_field :R, type: Integer, required: true
|
48
|
+
define_field :O, type: PDFByteString, required: true
|
49
|
+
define_field :OE, type: PDFByteString, version: '2.0'
|
50
|
+
define_field :U, type: PDFByteString, required: true
|
51
|
+
define_field :UE, type: PDFByteString, version: '2.0'
|
52
|
+
define_field :P, type: Integer, required: true
|
53
|
+
define_field :Perms, type: PDFByteString, version: '2.0'
|
54
|
+
define_field :EncryptMetadata, type: Boolean, default: true, version: '1.5'
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Validates the fields special for this encryption dictionary.
|
59
|
+
def perform_validation
|
60
|
+
super
|
61
|
+
case value[:R]
|
62
|
+
when 2, 3, 4
|
63
|
+
if value[:U].length != 32 || value[:O].length != 32
|
64
|
+
yield("Invalid size for /U or /O values for revisions <= 4", false)
|
65
|
+
end
|
66
|
+
when 6
|
67
|
+
if !key?(:OE) || !key?(:UE) || !key?(:Perms)
|
68
|
+
yield("Value of /OE, /UE or /Perms is missing for dictionary revision 6", false)
|
69
|
+
end
|
70
|
+
if value[:U].length != 48 || value[:O].length != 48 || value[:UE].length == 32 ||
|
71
|
+
value[:OE].length != 32 || value[:Perms].length != 16
|
72
|
+
yield("Invalid size for /U, /O, /UE, /OE or /Perms values for revisions 6", false)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
yield("Value of /R is not one of 2, 3, 4 or 6", false)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# The password-based standard security handler of the PDF specification, identified by a
|
83
|
+
# /Filter value of /Standard.
|
84
|
+
#
|
85
|
+
# == Overview
|
86
|
+
#
|
87
|
+
# The PDF specification defines one security handler that should be implemented by all PDF
|
88
|
+
# conform libraries and applications. This standard security handler allows access permissions
|
89
|
+
# and a user password as well as an owner password to be set. See
|
90
|
+
# StandardSecurityHandler::EncryptionOptions for all valid options that can be used with this
|
91
|
+
# security handler.
|
92
|
+
#
|
93
|
+
# The access permissions (see StandardSecurityHandler::Permissions) can be used to restrict what
|
94
|
+
# a user is allowed to do with a PDF file.
|
95
|
+
#
|
96
|
+
# When a user or owner password is specified, a PDF file can only be opened when the correct
|
97
|
+
# password is supplied.
|
98
|
+
#
|
99
|
+
# See: PDF1.7 s7.6.3, PDF2.0 s7.6.3
|
100
|
+
class StandardSecurityHandler < SecurityHandler
|
101
|
+
|
102
|
+
|
103
|
+
# Defines all available permissions.
|
104
|
+
#
|
105
|
+
# It is possible to use an array of permission symbols instead of an integer to describe the
|
106
|
+
# permission set. The used symbols are the lower case versions of the constants, i.e. the
|
107
|
+
# symbol for MODIFY_CONSTANT would be :modify_constant.
|
108
|
+
#
|
109
|
+
# See: PDF1.7 s7.6.3.2
|
110
|
+
module Permissions
|
111
|
+
|
112
|
+
# Printing (if HIGH_QUALITY_PRINT is also set, then high quality printing is allowed)
|
113
|
+
PRINT = 1 << 2
|
114
|
+
|
115
|
+
# Modification of the content by operations that are different from those controller by
|
116
|
+
# MODIFY_ANNOTATION, FILL_IN_FORMS and ASSEMBLE_DOCUMENT
|
117
|
+
MODIFY_CONTENT = 1 << 3
|
118
|
+
|
119
|
+
# Copying of content
|
120
|
+
COPY_CONTENT = 1 << 4
|
121
|
+
|
122
|
+
# Modifying annotations
|
123
|
+
MODIFY_ANNOTATION = 1 << 5
|
124
|
+
|
125
|
+
# Filling in form fields
|
126
|
+
FILL_IN_FORMS = 1 << 8
|
127
|
+
|
128
|
+
# Extracting content
|
129
|
+
EXTRACT_CONTENT = 1 << 9
|
130
|
+
|
131
|
+
# Assembling of the document (inserting, rotating or deleting of pages and creation of
|
132
|
+
# bookmarks or thumbnail images)
|
133
|
+
ASSEMBLE_DOCUMENT = 1 << 10
|
134
|
+
|
135
|
+
# High quality printing
|
136
|
+
HIGH_QUALITY_PRINT = 1 << 11
|
137
|
+
|
138
|
+
# Allows everything
|
139
|
+
ALL = PRINT | MODIFY_CONTENT | COPY_CONTENT | MODIFY_ANNOTATION | FILL_IN_FORMS |
|
140
|
+
EXTRACT_CONTENT | ASSEMBLE_DOCUMENT | HIGH_QUALITY_PRINT
|
141
|
+
|
142
|
+
# Reserved permission bits
|
143
|
+
RESERVED = 0xFFFFF000
|
144
|
+
|
145
|
+
# Maps permission symbols to their respective value
|
146
|
+
SYMBOL_TO_PERMISSION = {
|
147
|
+
print: PRINT,
|
148
|
+
modify_content: MODIFY_CONTENT,
|
149
|
+
copy_content: COPY_CONTENT,
|
150
|
+
modify_annotation: MODIFY_ANNOTATION,
|
151
|
+
fill_in_forms: FILL_IN_FORMS,
|
152
|
+
extract_content: EXTRACT_CONTENT,
|
153
|
+
assemble_document: ASSEMBLE_DOCUMENT,
|
154
|
+
high_quality_print: HIGH_QUALITY_PRINT,
|
155
|
+
}
|
156
|
+
|
157
|
+
# Maps a permission value to its symbol
|
158
|
+
PERMISSION_TO_SYMBOL = {
|
159
|
+
PRINT => :print,
|
160
|
+
MODIFY_CONTENT => :modify_content,
|
161
|
+
COPY_CONTENT => :copy_content,
|
162
|
+
MODIFY_ANNOTATION => :modify_annotation,
|
163
|
+
FILL_IN_FORMS => :fill_in_forms,
|
164
|
+
EXTRACT_CONTENT => :extract_content,
|
165
|
+
ASSEMBLE_DOCUMENT => :assemble_document,
|
166
|
+
HIGH_QUALITY_PRINT => :high_quality_print,
|
167
|
+
}
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
# Defines all possible options that can be passed to a StandardSecurityHandler when setting
|
173
|
+
# up encryption.
|
174
|
+
class EncryptionOptions
|
175
|
+
|
176
|
+
# The user password. If this attribute is not specified but the virtual +password+
|
177
|
+
# attribute is, then the latter is used.
|
178
|
+
attr_accessor :user_password
|
179
|
+
|
180
|
+
# The owner password. If this attribute is not specified but the virtual +password+
|
181
|
+
# attribute is, then the latter is used.
|
182
|
+
attr_accessor :owner_password
|
183
|
+
|
184
|
+
# The permissions. Either an integer with the needed permission bits set or an array of
|
185
|
+
# permission symbols.
|
186
|
+
#
|
187
|
+
# See: Permissions
|
188
|
+
attr_accessor :permissions
|
189
|
+
|
190
|
+
# The encryption algorithm.
|
191
|
+
attr_accessor :algorithm
|
192
|
+
|
193
|
+
# Specifies whether metadata should be encrypted.
|
194
|
+
attr_accessor :encrypt_metadata
|
195
|
+
|
196
|
+
# :nodoc:
|
197
|
+
def initialize(data = {})
|
198
|
+
fallback_pwd = data.delete(:password) { '' }
|
199
|
+
@user_password = data.delete(:user_password) { fallback_pwd }
|
200
|
+
@owner_password = data.delete(:owner_password) { fallback_pwd }
|
201
|
+
@permissions = process_permissions(data.delete(:permissions) { Permissions::ALL })
|
202
|
+
@algorithm = data.delete(:algorithm) { :arc4 }
|
203
|
+
@encrypt_metadata = data.delete(:encrypt_metadata) { true }
|
204
|
+
if data.size > 0
|
205
|
+
raise ArgumentError, "Invalid encryption options: #{data.keys.join(', ')}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
# Maps the permissions to an integer for use by the standard security handler.
|
212
|
+
def process_permissions(perms)
|
213
|
+
if perms.kind_of?(Array)
|
214
|
+
perms = perms.inject(0) do |result, perm|
|
215
|
+
result | Permissions::SYMBOL_TO_PERMISSION.fetch(perm, 0)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
Permissions::RESERVED | perms
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
|
223
|
+
# Additionally checks that the document trailer's ID has not changed.
|
224
|
+
#
|
225
|
+
# See: SecurityHandler#encryption_key_valid?
|
226
|
+
def encryption_key_valid?
|
227
|
+
super && trailer_id_hash == @trailer_id_hash
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns the permissions of the managed dictionary as array of symbol values.
|
231
|
+
#
|
232
|
+
# See: Permissions
|
233
|
+
def permissions
|
234
|
+
Permissions::PERMISSION_TO_SYMBOL.each_with_object([]) do |(perm, sym), result|
|
235
|
+
result << sym if dict[:P] & perm == perm
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
# Prepares the security handler for use in encrypting the document.
|
242
|
+
#
|
243
|
+
# See the attributes of the EncryptionOptions class for all possible arguments.
|
244
|
+
def prepare_encryption(**kwoptions)
|
245
|
+
options = EncryptionOptions.new(kwoptions)
|
246
|
+
|
247
|
+
dict[:Filter] = :Standard
|
248
|
+
dict[:R] = case dict[:V]
|
249
|
+
when 1 then 2
|
250
|
+
when 2 then 3
|
251
|
+
when 4 then 4
|
252
|
+
when 5 then 6
|
253
|
+
end
|
254
|
+
dict[:EncryptMetadata] = options.encrypt_metadata
|
255
|
+
dict[:P] = options.permissions
|
256
|
+
|
257
|
+
if dict[:V] >= 4
|
258
|
+
cfm = if options.algorithm == :arc4
|
259
|
+
:V2
|
260
|
+
elsif key_length == 16
|
261
|
+
:AESV2
|
262
|
+
else
|
263
|
+
:AESV3
|
264
|
+
end
|
265
|
+
dict[:CF] = {
|
266
|
+
StdCF: {
|
267
|
+
CFM: cfm,
|
268
|
+
AuthEvent: :DocOpen,
|
269
|
+
Length: key_length,
|
270
|
+
},
|
271
|
+
}
|
272
|
+
dict[:StmF] = dict[:StrF] = dict[:EFF] = :StdCF
|
273
|
+
end
|
274
|
+
|
275
|
+
if dict[:R] <= 4 && !document.trailer[:ID].kind_of?(Array)
|
276
|
+
document.trailer.set_random_id
|
277
|
+
end
|
278
|
+
|
279
|
+
options.user_password = prepare_password(options.user_password)
|
280
|
+
options.owner_password = prepare_password(options.owner_password)
|
281
|
+
|
282
|
+
dict[:O] = compute_o_field(options.owner_password, options.user_password)
|
283
|
+
dict[:U] = compute_u_field(options.user_password)
|
284
|
+
|
285
|
+
if dict[:R] <= 4
|
286
|
+
encryption_key = compute_user_encryption_key(options.user_password)
|
287
|
+
else
|
288
|
+
encryption_key = random_bytes(32)
|
289
|
+
dict[:UE] = compute_ue_field(options.user_password, encryption_key)
|
290
|
+
dict[:OE] = compute_oe_field(options.owner_password, encryption_key)
|
291
|
+
dict[:Perms] = compute_perms_field(encryption_key)
|
292
|
+
end
|
293
|
+
|
294
|
+
@trailer_id_hash = trailer_id_hash
|
295
|
+
[encryption_key, options.algorithm, options.algorithm, options.algorithm]
|
296
|
+
end
|
297
|
+
|
298
|
+
# Uses the given password (or the default password if none given) to retrieve the encryption
|
299
|
+
# key.
|
300
|
+
#
|
301
|
+
# If the optional +check_permissions+ argument is +true+, the permissions for files
|
302
|
+
# encrypted with revision 6 are checked. Otherwise, permission changes are ignored.
|
303
|
+
def prepare_decryption(password: '', check_permissions: true)
|
304
|
+
if dict[:Filter] != :Standard
|
305
|
+
raise(HexaPDF::UnsupportedEncryptionError,
|
306
|
+
"Invalid /Filter value for standard security handler")
|
307
|
+
elsif ![2, 3, 4, 6].include?(dict[:R])
|
308
|
+
raise(HexaPDF::UnsupportedEncryptionError,
|
309
|
+
"Invalid /R value for standard security handler")
|
310
|
+
elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(Array)
|
311
|
+
raise(HexaPDF::EncryptionError,
|
312
|
+
"Document ID for needed for decryption")
|
313
|
+
end
|
314
|
+
@trailer_id_hash = trailer_id_hash
|
315
|
+
|
316
|
+
password = prepare_password(password)
|
317
|
+
|
318
|
+
if user_password_valid?(prepare_password(''))
|
319
|
+
encryption_key = compute_user_encryption_key(prepare_password(''))
|
320
|
+
elsif user_password_valid?(password)
|
321
|
+
encryption_key = compute_user_encryption_key(password)
|
322
|
+
elsif owner_password_valid?(password)
|
323
|
+
encryption_key = compute_owner_encryption_key(password)
|
324
|
+
else
|
325
|
+
raise HexaPDF::EncryptionError, "Invalid password specified"
|
326
|
+
end
|
327
|
+
|
328
|
+
check_perms_field(encryption_key) if check_permissions && dict[:R] == 6
|
329
|
+
|
330
|
+
encryption_key
|
331
|
+
end
|
332
|
+
|
333
|
+
# Computes the hash value for the first string in the trailer ID array.
|
334
|
+
def trailer_id_hash # :nodoc:
|
335
|
+
id = document.unwrap(document.trailer[:ID])
|
336
|
+
(id.kind_of?(Array) ? id[0] : id).hash
|
337
|
+
end
|
338
|
+
|
339
|
+
# See SecurityHandler#encryption_dictionary_class
|
340
|
+
def encryption_dictionary_class
|
341
|
+
StandardEncryptionDictionary
|
342
|
+
end
|
343
|
+
|
344
|
+
# The padding used for passwords with fewer than 32 bytes. Only used for revisions <= 4.
|
345
|
+
#
|
346
|
+
# See: PDF1.7 s7.6.3.3
|
347
|
+
PASSWORD_PADDING = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08" \
|
348
|
+
"\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A".b
|
349
|
+
|
350
|
+
# Computes the user encryption key.
|
351
|
+
#
|
352
|
+
# For revisions <= 4 this is the *only* way for generating the encryption key needed to
|
353
|
+
# encrypt or decrypt a file.
|
354
|
+
#
|
355
|
+
# For revision 6 the file encryption key is a string of random bytes that has been encrypted
|
356
|
+
# with the user password. If the password is the owner password,
|
357
|
+
# #compute_owner_encryption_key has to be used instead.
|
358
|
+
#
|
359
|
+
# See: PDF1.7 s7.6.3.3 (algorithm 2), PDF2.0 s7.6.3.3.2 (algorithm 2.A (a)-(b),(e))
|
360
|
+
def compute_user_encryption_key(password)
|
361
|
+
if dict[:R] <= 4
|
362
|
+
data = password
|
363
|
+
data += dict[:O]
|
364
|
+
data << [dict[:P]].pack('V')
|
365
|
+
data << document.trailer[:ID][0]
|
366
|
+
data << [0xFFFFFFFF].pack('V') if dict[:R] == 4 && !dict[:EncryptMetadata]
|
367
|
+
|
368
|
+
n = key_length
|
369
|
+
data = Digest::MD5.digest(data)
|
370
|
+
if dict[:R] >= 3
|
371
|
+
50.times { data = Digest::MD5.digest(data[0, n]) }
|
372
|
+
end
|
373
|
+
|
374
|
+
data[0, n]
|
375
|
+
elsif dict[:R] == 6
|
376
|
+
key = compute_hash(password, dict[:U][40, 8])
|
377
|
+
aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:UE])
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Computes the owner encryption key.
|
382
|
+
#
|
383
|
+
# For revisions <= 4 this is done by first retrieving the user password through the use of
|
384
|
+
# the owner password and then using the #compute_user_encryption_key method.
|
385
|
+
#
|
386
|
+
# For revision 6 file encryption key is a string of random bytes that has been encrypted
|
387
|
+
# with the owner password. If the password is the user password,
|
388
|
+
# #compute_user_encryption_key has to be used.
|
389
|
+
#
|
390
|
+
# See: PDF2.0 s7.6.3.3.2 (algorithm 2.A (a)-(d))
|
391
|
+
def compute_owner_encryption_key(password)
|
392
|
+
if dict[:R] <= 4
|
393
|
+
compute_user_encryption_key(user_password_from_owner_password(password))
|
394
|
+
elsif dict[:R] == 6
|
395
|
+
key = compute_hash(password, dict[:O][40, 8], dict[:U])
|
396
|
+
aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:OE])
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# Computes the encryption dictionary's /O (owner password) value.
|
401
|
+
#
|
402
|
+
# Short explanation: For revisions <= 4 the user password is encrypted with a key based on
|
403
|
+
# the owner password. For revision 6 the /O value is a hash computed from the password and
|
404
|
+
# the /U value with added validation and key salts.
|
405
|
+
#
|
406
|
+
# *Attention*: If revision 6 is used, the /U value has to be computed and set before this
|
407
|
+
# method is used, otherwise the return value is incorrect!
|
408
|
+
#
|
409
|
+
# See: PDF1.7 s7.6.3.4 (algorithm 3), PDF2.0 s7.6.3.4.7 (algorithm 9 (a))
|
410
|
+
def compute_o_field(owner_password, user_password)
|
411
|
+
if dict[:R] <= 4
|
412
|
+
data = Digest::MD5.digest(owner_password)
|
413
|
+
if dict[:R] >= 3
|
414
|
+
50.times { data = Digest::MD5.digest(data) }
|
415
|
+
end
|
416
|
+
key = data[0, key_length]
|
417
|
+
|
418
|
+
data = arc4_algorithm.encrypt(key, user_password)
|
419
|
+
if dict[:R] >= 3
|
420
|
+
19.times {|i| data = arc4_algorithm.encrypt(xor_key(key, i + 1), data)}
|
421
|
+
end
|
422
|
+
|
423
|
+
data
|
424
|
+
elsif dict[:R] == 6
|
425
|
+
validation_salt = random_bytes(8)
|
426
|
+
key_salt = random_bytes(8)
|
427
|
+
compute_hash(owner_password, validation_salt, dict[:U]) << validation_salt << key_salt
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# Computes the encryption dictionary's /OE (owner encryption key) value (for revision 6
|
432
|
+
# only).
|
433
|
+
#
|
434
|
+
# Short explanation: Encrypts the file encryption key with a key based on the password and
|
435
|
+
# the /O and /U values.
|
436
|
+
#
|
437
|
+
# See: PDF2.0 s7.6.3.4.7 (algorithm 9 (b))
|
438
|
+
def compute_oe_field(password, file_encryption_key)
|
439
|
+
key = compute_hash(password, dict[:O][40, 8], dict[:U])
|
440
|
+
aes_algorithm.new(key, "\0" * 16, :encrypt).process(file_encryption_key)
|
441
|
+
end
|
442
|
+
|
443
|
+
# Computes the encryption dictionary's /U (user password) value.
|
444
|
+
#
|
445
|
+
# Short explanation: For revisions <= 4, the password padding string is encrypted with a key
|
446
|
+
# based on the user password. For revision 6 the /U value is a hash computed from the
|
447
|
+
# password with added validation and key salts.
|
448
|
+
#
|
449
|
+
# See: PDF1.7 s7.6.3.4 (algorithm 4 for R=2, algorithm 5 for R=3 and R=4)
|
450
|
+
# PDF2.0 s7.6.3.4.6 (algorithm 8 (a) for R=6)
|
451
|
+
def compute_u_field(password)
|
452
|
+
if dict[:R] == 2
|
453
|
+
key = compute_user_encryption_key(password)
|
454
|
+
arc4_algorithm.encrypt(key, PASSWORD_PADDING)
|
455
|
+
elsif dict[:R] <= 4
|
456
|
+
key = compute_user_encryption_key(password)
|
457
|
+
data = Digest::MD5.digest(PASSWORD_PADDING + document.trailer[:ID][0])
|
458
|
+
data = arc4_algorithm.encrypt(key, data)
|
459
|
+
19.times {|i| data = arc4_algorithm.encrypt(xor_key(key, i + 1), data)}
|
460
|
+
data << "hexapdfhexapdfhe"
|
461
|
+
elsif dict[:R] == 6
|
462
|
+
validation_salt = random_bytes(8)
|
463
|
+
key_salt = random_bytes(8)
|
464
|
+
compute_hash(password, validation_salt) << validation_salt << key_salt
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Computes the encryption dictionary's /UE (user encryption key) value (for revision 6
|
469
|
+
# only).
|
470
|
+
#
|
471
|
+
# Short explanation: Encrypts the file encryption key with a key based on the password and
|
472
|
+
# the /U value.
|
473
|
+
#
|
474
|
+
# See: PDF2.0 s7.6.3.4.6 (algorithm 8 (b))
|
475
|
+
def compute_ue_field(password, file_encryption_key)
|
476
|
+
key = compute_hash(password, dict[:U][40, 8])
|
477
|
+
aes_algorithm.new(key, "\0" * 16, :encrypt).process(file_encryption_key)
|
478
|
+
end
|
479
|
+
|
480
|
+
# Computes the encryption dictionary's /Perms (permissions) value (for revision 6 only).
|
481
|
+
#
|
482
|
+
# Uses /P and /EncryptMetadata values, so these have to be set beforehand.
|
483
|
+
#
|
484
|
+
# See: PDF2.0 s7.6.3.4.8 (algorithm 10)
|
485
|
+
def compute_perms_field(file_encryption_key)
|
486
|
+
data = [dict[:P]].pack('V')
|
487
|
+
data << [0xFFFFFFFF].pack('V')
|
488
|
+
data << (dict[:EncryptMetadata] ? 'T' : 'F')
|
489
|
+
data << 'adb'
|
490
|
+
data << 'hexa'
|
491
|
+
aes_algorithm.new(file_encryption_key, "\0" * 16, :encrypt).process(data)
|
492
|
+
end
|
493
|
+
|
494
|
+
# Authenticates the user password, i.e. decides whether the given user password is valid.
|
495
|
+
#
|
496
|
+
# See: PDF1.7 s7.6.3.4 (algorithm 6), PDF2.0 s7.6.3.4.9 (algorithm 11)
|
497
|
+
def user_password_valid?(password)
|
498
|
+
if dict[:R] == 2
|
499
|
+
compute_u_field(password) == dict[:U]
|
500
|
+
elsif dict[:R] <= 4
|
501
|
+
compute_u_field(password)[0, 16] == dict[:U][0, 16]
|
502
|
+
elsif dict[:R] == 6
|
503
|
+
compute_hash(password, dict[:U][32, 8]) == dict[:U][0, 32]
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
# Authenticates the owner password, i.e. decides whether the given owner password is valid.
|
508
|
+
#
|
509
|
+
# See: PDF1.7 s7.6.3.4 (algorithm 7), PDF2.0 s7.6.3.4.10 (algorithm 12)
|
510
|
+
def owner_password_valid?(password)
|
511
|
+
if dict[:R] <= 4
|
512
|
+
user_password_valid?(user_password_from_owner_password(password))
|
513
|
+
elsif dict[:R] == 6
|
514
|
+
compute_hash(password, dict[:O][32, 8], dict[:U]) == dict[:O][0, 32]
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# Checks if the decrypted /Perms entry matches the /P and /EncryptMetadata entries.
|
519
|
+
#
|
520
|
+
# This method can only be used for revision 6.
|
521
|
+
#
|
522
|
+
# See: PDF2.0 s7.6.3.4.11 (algorithm 13)
|
523
|
+
def check_perms_field(encryption_key)
|
524
|
+
decrypted = aes_algorithm.new(encryption_key, "\0" * 16, :decrypt).process(dict[:Perms])
|
525
|
+
if decrypted[9, 3] != "adb"
|
526
|
+
raise HexaPDF::EncryptionError, "/Perms field cannot be decrypted"
|
527
|
+
elsif (dict[:P] & 0xFFFFFFFF) != (decrypted[0, 4].unpack('V').first & 0xFFFFFFFF)
|
528
|
+
raise HexaPDF::EncryptionError, "Decrypted permissions don't match /P"
|
529
|
+
elsif decrypted[8] != (dict[:EncryptMetadata] ? 'T' : 'F')
|
530
|
+
raise HexaPDF::EncryptionError, "Decrypted /Perms field doesn't match /EncryptMetadata"
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# Returns the user password when given the owner password for revisions <= 4.
|
535
|
+
#
|
536
|
+
# See: PDF1.7 s7.6.3.4 (algorithm 7 (a) and (b))
|
537
|
+
def user_password_from_owner_password(owner_password)
|
538
|
+
data = Digest::MD5.digest(owner_password)
|
539
|
+
if dict[:R] >= 3
|
540
|
+
50.times { data = Digest::MD5.digest(data) }
|
541
|
+
end
|
542
|
+
key = data[0, key_length]
|
543
|
+
|
544
|
+
if dict[:R] == 2
|
545
|
+
userpwd = arc4_algorithm.decrypt(key, dict[:O])
|
546
|
+
else
|
547
|
+
userpwd = dict[:O]
|
548
|
+
20.times {|i| userpwd = arc4_algorithm.decrypt(xor_key(key, 19 - i), userpwd)}
|
549
|
+
end
|
550
|
+
|
551
|
+
userpwd
|
552
|
+
end
|
553
|
+
|
554
|
+
# Computes a hash that is used extensively for all operations in security handlers of
|
555
|
+
# revision 6.
|
556
|
+
#
|
557
|
+
# Note: The original input (as defined by the spec) is calculated as
|
558
|
+
# "#{password}#{salt}#{user_key}" where +user_key+ has to be empty when doing operations
|
559
|
+
# with the user password.
|
560
|
+
#
|
561
|
+
# See: PDF2.0 s7.6.3.3.3 (algorithm 2.B)
|
562
|
+
def compute_hash(password, salt, user_key = '')
|
563
|
+
k = Digest::SHA256.digest("#{password}#{salt}#{user_key}")
|
564
|
+
e = ''
|
565
|
+
|
566
|
+
i = 0
|
567
|
+
while i < 64 || e.getbyte(-1) > i - 32
|
568
|
+
k1 = "#{password}#{k}#{user_key}" * 64
|
569
|
+
e = aes_algorithm.new(k[0, 16], k[16, 16], :encrypt).process(k1)
|
570
|
+
k = case e.unpack('C16').inject(&:+) % 3 # 256 % 3 == 1 % 3 --> x*256 % 3 == x % 3
|
571
|
+
when 0 then Digest::SHA256.digest(e)
|
572
|
+
when 1 then Digest::SHA384.digest(e)
|
573
|
+
when 2 then Digest::SHA512.digest(e)
|
574
|
+
end
|
575
|
+
i += 1
|
576
|
+
end
|
577
|
+
|
578
|
+
k[0, 32]
|
579
|
+
end
|
580
|
+
|
581
|
+
# Returns the password modified so that if follows certain rules:
|
582
|
+
#
|
583
|
+
# * For revisions <= 4, the password is converted into ISO-8859-1 encoding, padded with
|
584
|
+
# PASSWORD_PADDING and truncated to a maximum of 32 bytes.
|
585
|
+
#
|
586
|
+
# * For revision 6 the password is converted into UTF-8 encoding that is normalized
|
587
|
+
# according to the PDF2.0 specification.
|
588
|
+
#
|
589
|
+
# See: PDF1.7 s7.6.3.3 (algorithm 2 step a)),
|
590
|
+
# PDF2.0 s7.6.3.3.2 (algorithm 2.A steps a) and b))
|
591
|
+
def prepare_password(password)
|
592
|
+
if dict[:R] <= 4
|
593
|
+
password.to_s[0, 32].encode(Encoding::ISO_8859_1).force_encoding(Encoding::BINARY).
|
594
|
+
ljust(32, PASSWORD_PADDING)
|
595
|
+
elsif dict[:R] == 6
|
596
|
+
password.to_s.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)[0, 127]
|
597
|
+
end
|
598
|
+
rescue Encoding::UndefinedConversionError => e
|
599
|
+
raise HexaPDF::EncryptionError, "Invalid character in password: #{e.error_char}"
|
600
|
+
end
|
601
|
+
|
602
|
+
# XORs each byte of the String +key+ with value and returns the resulting string.
|
603
|
+
def xor_key(key, value)
|
604
|
+
new_key = key.dup
|
605
|
+
i = 0
|
606
|
+
while i < new_key.length
|
607
|
+
new_key.setbyte(i, new_key.getbyte(i) ^ value)
|
608
|
+
i += 1
|
609
|
+
end
|
610
|
+
new_key
|
611
|
+
end
|
612
|
+
|
613
|
+
end
|
614
|
+
|
615
|
+
end
|
616
|
+
end
|