hexapdf 0.27.0 → 0.29.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +100 -11
- data/examples/019-acro_form.rb +14 -3
- data/examples/023-images.rb +30 -0
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +2 -2
- data/lib/hexapdf/configuration.rb +13 -14
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +1 -5
- data/lib/hexapdf/dictionary_fields.rb +6 -2
- data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
- data/lib/hexapdf/digital_signature/handler.rb +138 -0
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
- data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
- data/lib/hexapdf/digital_signature/signatures.rb +210 -0
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
- data/lib/hexapdf/digital_signature/signing.rb +101 -0
- data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
- data/lib/hexapdf/digital_signature.rb +56 -0
- data/lib/hexapdf/document.rb +27 -24
- data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +32 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +13 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +4 -0
- data/lib/hexapdf/tokenizer.rb +14 -8
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +33 -7
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/page.rb +56 -46
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +2 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/{type/signature → digital_signature}/common.rb +34 -4
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
- data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
- data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
- data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
- data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
- data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
- data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
- data/test/hexapdf/digital_signature/test_signing.rb +53 -0
- data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_dictionary_fields.rb +2 -1
- data/test/hexapdf/test_document.rb +3 -9
- data/test/hexapdf/test_importer.rb +13 -6
- data/test/hexapdf/test_parser.rb +17 -0
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +43 -0
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +3 -4
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +18 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +3 -0
- data/test/hexapdf/type/test_page.rb +67 -30
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- metadata +69 -16
- data/lib/hexapdf/document/signatures.rb +0 -546
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
- data/lib/hexapdf/type/signature/handler.rb +0 -140
- data/test/hexapdf/document/test_signatures.rb +0 -352
@@ -0,0 +1,148 @@
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# This file is part of HexaPDF.
|
5
|
+
#
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
7
|
+
# Copyright (C) 2014-2022 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
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
|
+
#++
|
36
|
+
|
37
|
+
require 'openssl'
|
38
|
+
require 'net/http'
|
39
|
+
require 'hexapdf/error'
|
40
|
+
require 'stringio'
|
41
|
+
|
42
|
+
module HexaPDF
|
43
|
+
module DigitalSignature
|
44
|
+
module Signing
|
45
|
+
|
46
|
+
# This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
|
47
|
+
# document. It is registered under the :timestamp name.
|
48
|
+
#
|
49
|
+
# The timestamp is provided by a timestamp authority and establishes the document contents at
|
50
|
+
# the time indicated in the timestamp. Timestamping a PDF document is usually done in context
|
51
|
+
# of long term validation but can also be done standalone.
|
52
|
+
#
|
53
|
+
# == Usage
|
54
|
+
#
|
55
|
+
# It is necessary to provide at least the URL of the timestamp authority server (TSA) via
|
56
|
+
# #tsa_url, everything else is optional and uses default values. The TSA server must not use
|
57
|
+
# authentication to be usable.
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
#
|
61
|
+
# document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
|
62
|
+
class TimestampHandler
|
63
|
+
|
64
|
+
# The URL of the timestamp authority server.
|
65
|
+
#
|
66
|
+
# This value is required.
|
67
|
+
attr_accessor :tsa_url
|
68
|
+
|
69
|
+
# The hash algorithm to use for timestamping. Defaults to SHA512.
|
70
|
+
attr_accessor :tsa_hash_algorithm
|
71
|
+
|
72
|
+
# The policy OID to use for timestamping. Defaults to +nil+.
|
73
|
+
attr_accessor :tsa_policy_id
|
74
|
+
|
75
|
+
# The size of the serialized signature that should be reserved.
|
76
|
+
#
|
77
|
+
# If this attribute has not been set, an empty string will be signed using #sign to
|
78
|
+
# determine the signature size which will contact the TSA server
|
79
|
+
#
|
80
|
+
# The size needs to be at least as big as the final signature, otherwise signing results in
|
81
|
+
# an error.
|
82
|
+
attr_writer :signature_size
|
83
|
+
|
84
|
+
# The reason for timestamping. If used, will be set on the signature object.
|
85
|
+
attr_accessor :reason
|
86
|
+
|
87
|
+
# The timestamping location. If used, will be set on the signature object.
|
88
|
+
attr_accessor :location
|
89
|
+
|
90
|
+
# The contact information. If used, will be set on the signature object.
|
91
|
+
attr_accessor :contact_info
|
92
|
+
|
93
|
+
# Creates a new TimestampHandler with the given attributes.
|
94
|
+
def initialize(**arguments)
|
95
|
+
@signature_size = nil
|
96
|
+
arguments.each {|name, value| send("#{name}=", value) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the size of the serialized signature that should be reserved.
|
100
|
+
def signature_size
|
101
|
+
@signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
# Finalizes the signature field as well as the signature dictionary before writing.
|
105
|
+
def finalize_objects(_signature_field, signature)
|
106
|
+
signature.document.version = '2.0'
|
107
|
+
signature[:Type] = :DocTimeStamp
|
108
|
+
signature[:Filter] = :'Adobe.PPKLite'
|
109
|
+
signature[:SubFilter] = :'ETSI.RFC3161'
|
110
|
+
signature[:Reason] = reason if reason
|
111
|
+
signature[:Location] = location if location
|
112
|
+
signature[:ContactInfo] = contact_info if contact_info
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
|
116
|
+
# given IO byte ranges.
|
117
|
+
def sign(io, byte_range)
|
118
|
+
hash_algorithm = tsa_hash_algorithm || 'SHA512'
|
119
|
+
digest = OpenSSL::Digest.new(hash_algorithm)
|
120
|
+
io.pos = byte_range[0]
|
121
|
+
digest << io.read(byte_range[1])
|
122
|
+
io.pos = byte_range[2]
|
123
|
+
digest << io.read(byte_range[3])
|
124
|
+
|
125
|
+
req = OpenSSL::Timestamp::Request.new
|
126
|
+
req.algorithm = hash_algorithm
|
127
|
+
req.message_imprint = digest.digest
|
128
|
+
req.policy_id = tsa_policy_id if tsa_policy_id
|
129
|
+
|
130
|
+
http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
|
131
|
+
'content-type' => 'application/timestamp-query')
|
132
|
+
if http_response.kind_of?(Net::HTTPOK)
|
133
|
+
response = OpenSSL::Timestamp::Response.new(http_response.body)
|
134
|
+
if response.status == 0
|
135
|
+
response.token.to_der
|
136
|
+
else
|
137
|
+
raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# This file is part of HexaPDF.
|
5
|
+
#
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
7
|
+
# Copyright (C) 2014-2022 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
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
|
+
#++
|
36
|
+
|
37
|
+
require 'hexapdf/document'
|
38
|
+
|
39
|
+
module HexaPDF
|
40
|
+
module DigitalSignature
|
41
|
+
|
42
|
+
# This module contains everything related to the signing of a PDF document, i.e. signing
|
43
|
+
# handlers and the actual code for signing.
|
44
|
+
module Signing
|
45
|
+
|
46
|
+
autoload(:DefaultHandler, 'hexapdf/digital_signature/signing/default_handler')
|
47
|
+
autoload(:TimestampHandler, 'hexapdf/digital_signature/signing/timestamp_handler')
|
48
|
+
autoload(:SignedDataCreator, 'hexapdf/digital_signature/signing/signed_data_creator')
|
49
|
+
|
50
|
+
# Embeds the given +signature+ into the /Contents value of the newest signature dictionary of
|
51
|
+
# the PDF document given by the +io+ argument.
|
52
|
+
#
|
53
|
+
# This functionality can be used together with the support for external signing (see
|
54
|
+
# DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.
|
55
|
+
#
|
56
|
+
# Note: This will, most probably, only work on documents prepared for external signing by
|
57
|
+
# HexaPDF and not by other libraries.
|
58
|
+
def self.embed_signature(io, signature)
|
59
|
+
doc = HexaPDF::Document.new(io: io)
|
60
|
+
signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
|
61
|
+
signature_dict_offset, signature_dict_length = locate_signature_dict(
|
62
|
+
doc.revisions.current.xref_section,
|
63
|
+
doc.revisions.parser.startxref_offset,
|
64
|
+
signature_dict.oid
|
65
|
+
)
|
66
|
+
io.pos = signature_dict_offset
|
67
|
+
signature_data = io.read(signature_dict_length)
|
68
|
+
replace_signature_contents(signature_data, signature)
|
69
|
+
io.pos = signature_dict_offset
|
70
|
+
io.write(signature_data)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Uses the information in the given cross-reference section as well as the byte offset of the
|
74
|
+
# cross-reference section to calculate the offset and length of the signature dictionary with
|
75
|
+
# the given object id.
|
76
|
+
def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
|
77
|
+
data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
|
78
|
+
[start_xref_position, nil]
|
79
|
+
index = data.index {|_pos, oid| oid == signature_oid }
|
80
|
+
[data[index][0], data[index + 1][0] - data[index][0]]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Replaces the value of the /Contents key in the serialized +signature_data+ with the value of
|
84
|
+
# +contents+.
|
85
|
+
def self.replace_signature_contents(signature_data, contents)
|
86
|
+
signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
|
87
|
+
length = match.size
|
88
|
+
result = "Contents<#{contents.unpack1('H*')}"
|
89
|
+
if length < result.size
|
90
|
+
raise HexaPDF::Error, "The reserved space for the signature was too small " \
|
91
|
+
"(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
|
92
|
+
"#signature_size method to increase the reserved space"
|
93
|
+
end
|
94
|
+
"#{result.ljust(length - 1, '0')}>"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -34,59 +34,55 @@
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
35
|
#++
|
36
36
|
|
37
|
-
require 'hexapdf/type/signature'
|
38
|
-
|
39
37
|
module HexaPDF
|
40
|
-
module
|
41
|
-
class Signature
|
38
|
+
module DigitalSignature
|
42
39
|
|
43
|
-
|
44
|
-
|
40
|
+
# Holds the result information when verifying a signature.
|
41
|
+
class VerificationResult
|
45
42
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
# :nodoc:
|
44
|
+
MESSAGE_SORT_MAP = {
|
45
|
+
info: {warning: 1, error: 1, info: 0},
|
46
|
+
warning: {info: -1, error: 1, warning: 0},
|
47
|
+
error: {info: -1, warning: -1, error: 0},
|
48
|
+
}
|
52
49
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
50
|
+
# This structure represents a single status message, containing the type (:info, :warning,
|
51
|
+
# :error) and the content of the message.
|
52
|
+
Message = Struct.new(:type, :content) do
|
53
|
+
def <=>(other)
|
54
|
+
MESSAGE_SORT_MAP[type][other.type]
|
59
55
|
end
|
56
|
+
end
|
60
57
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
# Creates an empty result object.
|
65
|
-
def initialize
|
66
|
-
@messages = []
|
67
|
-
end
|
58
|
+
# An array with all result messages.
|
59
|
+
attr_reader :messages
|
68
60
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
61
|
+
# Creates an empty result object.
|
62
|
+
def initialize
|
63
|
+
@messages = []
|
64
|
+
end
|
73
65
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
66
|
+
# Returns +true+ if there are no error messages.
|
67
|
+
def success?
|
68
|
+
@messages.none? {|message| message.type == :error }
|
69
|
+
end
|
78
70
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# +content+:: The log message.
|
84
|
-
def log(type, content)
|
85
|
-
@messages << Message.new(type, content)
|
86
|
-
end
|
71
|
+
# Returns +true+ if there is at least one error message.
|
72
|
+
def failure?
|
73
|
+
!success?
|
74
|
+
end
|
87
75
|
|
76
|
+
# Adds a new message of the given type to this result object.
|
77
|
+
#
|
78
|
+
# +type+:: One of :info, :warning or :error.
|
79
|
+
#
|
80
|
+
# +content+:: The log message.
|
81
|
+
def log(type, content)
|
82
|
+
@messages << Message.new(type, content)
|
88
83
|
end
|
89
84
|
|
90
85
|
end
|
86
|
+
|
91
87
|
end
|
92
88
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# This file is part of HexaPDF.
|
5
|
+
#
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
7
|
+
# Copyright (C) 2014-2022 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
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
|
+
#++
|
36
|
+
|
37
|
+
module HexaPDF
|
38
|
+
|
39
|
+
# PDF documents can be signed using digital signatures. Such a signature can be used to
|
40
|
+
# authenticate the identity of the signer and the contents of the documents.
|
41
|
+
#
|
42
|
+
# This module contains all code related to digital signatures in PDF.
|
43
|
+
#
|
44
|
+
# See: PDF1.7/2.0 s12.8
|
45
|
+
module DigitalSignature
|
46
|
+
|
47
|
+
autoload(:Signatures, 'hexapdf/digital_signature/signatures')
|
48
|
+
autoload(:Signature, "hexapdf/digital_signature/signature")
|
49
|
+
autoload(:Handler, 'hexapdf/digital_signature/handler')
|
50
|
+
autoload(:CMSHandler, "hexapdf/digital_signature/cms_handler")
|
51
|
+
autoload(:PKCS1Handler, "hexapdf/digital_signature/pkcs1_handler")
|
52
|
+
autoload(:VerificationResult, 'hexapdf/digital_signature/verification_result')
|
53
|
+
autoload(:Signing, 'hexapdf/digital_signature/signing')
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
data/lib/hexapdf/document.rb
CHANGED
@@ -52,6 +52,7 @@ require 'hexapdf/importer'
|
|
52
52
|
require 'hexapdf/image_loader'
|
53
53
|
require 'hexapdf/font_loader'
|
54
54
|
require 'hexapdf/layout'
|
55
|
+
require 'hexapdf/digital_signature'
|
55
56
|
|
56
57
|
begin
|
57
58
|
require 'hexapdf/cext'
|
@@ -105,7 +106,6 @@ module HexaPDF
|
|
105
106
|
autoload(:Fonts, 'hexapdf/document/fonts')
|
106
107
|
autoload(:Images, 'hexapdf/document/images')
|
107
108
|
autoload(:Files, 'hexapdf/document/files')
|
108
|
-
autoload(:Signatures, 'hexapdf/document/signatures')
|
109
109
|
autoload(:Destinations, 'hexapdf/document/destinations')
|
110
110
|
autoload(:Layout, 'hexapdf/document/layout')
|
111
111
|
|
@@ -152,18 +152,24 @@ module HexaPDF
|
|
152
152
|
#
|
153
153
|
# Options:
|
154
154
|
#
|
155
|
-
# io::
|
156
|
-
#
|
155
|
+
# io::
|
156
|
+
# If an IO object is provided, then this document can read PDF objects from this IO object,
|
157
|
+
# otherwise it can only contain created PDF objects.
|
157
158
|
#
|
158
|
-
# decryption_opts::
|
159
|
+
# decryption_opts::
|
160
|
+
# A hash with options for decrypting the PDF objects loaded from the IO. The PDF standard
|
161
|
+
# security handler expects a :password key to be set to either the user or owner password of
|
162
|
+
# the PDF file.
|
159
163
|
#
|
160
|
-
# config::
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
+
# config::
|
165
|
+
# A hash with configuration options that is deep-merged into the default configuration (see
|
166
|
+
# HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration], meaning
|
167
|
+
# that direct sub-hashes are merged instead of overwritten.
|
164
168
|
def initialize(io: nil, decryption_opts: {}, config: {})
|
165
169
|
@config = Configuration.with_defaults(config)
|
166
170
|
@version = '1.2'
|
171
|
+
@cache = Hash.new {|h, k| h[k] = {} }
|
172
|
+
@listeners = {}
|
167
173
|
|
168
174
|
@revisions = Revisions.from_io(self, io)
|
169
175
|
@security_handler = if encrypted? && @config['document.auto_decrypt']
|
@@ -171,9 +177,6 @@ module HexaPDF
|
|
171
177
|
else
|
172
178
|
nil
|
173
179
|
end
|
174
|
-
|
175
|
-
@listeners = {}
|
176
|
-
@cache = Hash.new {|h, k| h[k] = {} }
|
177
180
|
end
|
178
181
|
|
179
182
|
# :call-seq:
|
@@ -251,19 +254,16 @@ module HexaPDF
|
|
251
254
|
# :call-seq:
|
252
255
|
# doc.import(obj) -> imported_object
|
253
256
|
#
|
254
|
-
# Imports the given
|
257
|
+
# Imports the given object from a different HexaPDF::Document instance and returns the imported
|
255
258
|
# object.
|
256
259
|
#
|
257
260
|
# If the same argument is provided in multiple invocations, the import is done only once and
|
258
|
-
# the previously
|
261
|
+
# the previously imported object is returned.
|
259
262
|
#
|
260
263
|
# See: Importer
|
261
264
|
def import(obj)
|
262
|
-
|
263
|
-
|
264
|
-
"with another document"
|
265
|
-
end
|
266
|
-
HexaPDF::Importer.for(source: obj.document, destination: self).import(obj)
|
265
|
+
source = (obj.kind_of?(HexaPDF::Object) ? obj.document : nil)
|
266
|
+
HexaPDF::Importer.for(self).import(obj, source: source)
|
267
267
|
end
|
268
268
|
|
269
269
|
# Wraps the given object inside a HexaPDF::Object class which allows one to use
|
@@ -589,25 +589,28 @@ module HexaPDF
|
|
589
589
|
acro_form&.signature_flag?(:signatures_exist)
|
590
590
|
end
|
591
591
|
|
592
|
-
# Returns
|
592
|
+
# Returns a DigitalSignature::Signatures object that allows working with the digital signatures
|
593
|
+
# of this document.
|
593
594
|
def signatures
|
594
|
-
@signatures ||= Signatures.new(self)
|
595
|
+
@signatures ||= DigitalSignature::Signatures.new(self)
|
595
596
|
end
|
596
597
|
|
597
598
|
# Signs the document and writes it to the given file or IO object.
|
598
599
|
#
|
599
600
|
# For details on the arguments +file_or_io+, +signature+ and +write_options+ see
|
600
|
-
# HexaPDF::
|
601
|
+
# HexaPDF::DigitalSignature::Signatures#add.
|
601
602
|
#
|
602
603
|
# The signing handler to be used is determined by the +handler+ argument together with the rest
|
603
|
-
# of the keyword arguments (see HexaPDF::
|
604
|
+
# of the keyword arguments (see HexaPDF::DigitalSignature::Signatures#signing_handler for
|
605
|
+
# details).
|
604
606
|
#
|
605
|
-
# If not changed, the default signing handler is
|
607
|
+
# If not changed, the default signing handler is
|
608
|
+
# HexaPDF::DigitalSignature::Signing::DefaultHandler.
|
606
609
|
#
|
607
610
|
# *Note*: Once signing is done the document cannot be changed anymore since it was written. If a
|
608
611
|
# document needs to be signed multiple times, it needs to be loaded again after writing.
|
609
612
|
def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
|
610
|
-
handler = signatures.
|
613
|
+
handler = signatures.signing_handler(name: handler, **handler_options)
|
611
614
|
signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
|
612
615
|
end
|
613
616
|
|
@@ -97,7 +97,8 @@ module HexaPDF
|
|
97
97
|
# a user is allowed to do with a PDF file.
|
98
98
|
#
|
99
99
|
# When a user or owner password is specified, a PDF file can only be opened when the correct
|
100
|
-
# password is supplied.
|
100
|
+
# password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to
|
101
|
+
# HexaPDF::Document.new needs to contain a :password key with the password.
|
101
102
|
#
|
102
103
|
# See: PDF1.7 s7.6.3, PDF2.0 s7.6.3
|
103
104
|
class StandardSecurityHandler < SecurityHandler
|
data/lib/hexapdf/importer.rb
CHANGED
@@ -60,64 +60,69 @@ module HexaPDF
|
|
60
60
|
|
61
61
|
end
|
62
62
|
|
63
|
-
# Returns the Importer object for copying objects
|
64
|
-
|
65
|
-
def self.for(source:, destination:)
|
63
|
+
# Returns the Importer object for copying objects to the +destination+ document.
|
64
|
+
def self.for(destination)
|
66
65
|
@map ||= {}
|
67
|
-
@map.keep_if {|_, v| v.
|
68
|
-
source = NullableWeakRef.new(source)
|
66
|
+
@map.keep_if {|_, v| v.destination.weakref_alive? }
|
69
67
|
destination = NullableWeakRef.new(destination)
|
70
|
-
@map[
|
68
|
+
@map[destination.hash] ||= new(destination)
|
71
69
|
end
|
72
70
|
|
73
71
|
private_class_method :new
|
74
72
|
|
75
|
-
attr_reader :
|
73
|
+
attr_reader :destination #:nodoc:
|
76
74
|
|
77
|
-
# Initializes a new importer that can import objects
|
78
|
-
|
79
|
-
def initialize(source:, destination:)
|
80
|
-
@source = source
|
75
|
+
# Initializes a new importer that can import objects to the +destination+ document.
|
76
|
+
def initialize(destination)
|
81
77
|
@destination = destination
|
82
78
|
@mapper = {}
|
83
79
|
end
|
84
80
|
|
85
|
-
|
86
|
-
|
81
|
+
SourceWrapper = Struct.new(:source) #:nodoc:
|
82
|
+
|
83
|
+
# Imports the given +object+ to the destination object and returns the imported object.
|
87
84
|
#
|
88
85
|
# Note: Indirect objects are automatically added to the destination document but direct or
|
89
86
|
# simple objects are not.
|
90
87
|
#
|
91
|
-
#
|
92
|
-
|
88
|
+
# The +source+ argument should be +nil+ or set to the source document of the imported object. If
|
89
|
+
# it is +nil+, the source document is dynamically identified. If this identification is not
|
90
|
+
# possible and the source document would be needed, an error is raised.
|
91
|
+
def import(object, source: nil)
|
92
|
+
internal_import(object, SourceWrapper.new(source))
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Does the actual importing of the given +object+, using +wrapper+ to store/use the source
|
98
|
+
# document.
|
99
|
+
def internal_import(object, wrapper)
|
93
100
|
mapped_object = @mapper[object.data]&.__getobj__ if object.kind_of?(HexaPDF::Object)
|
94
|
-
if
|
95
|
-
raise HexaPDF::Error, "Import error: Incorrect document object for importer"
|
96
|
-
elsif mapped_object && !mapped_object.null?
|
101
|
+
if mapped_object && !mapped_object.null?
|
97
102
|
if object.class != mapped_object.class
|
98
103
|
mapped_object = @destination.wrap(mapped_object, type: object.class)
|
99
104
|
end
|
100
105
|
mapped_object
|
101
106
|
else
|
102
|
-
duplicate(object)
|
107
|
+
duplicate(object, wrapper)
|
103
108
|
end
|
104
109
|
end
|
105
110
|
|
106
|
-
private
|
107
|
-
|
108
111
|
# Recursively duplicates the object.
|
109
112
|
#
|
110
113
|
# PDF objects are automatically added to the destination document if they are indirect objects
|
111
114
|
# in the source document.
|
112
|
-
def duplicate(object)
|
115
|
+
def duplicate(object, wrapper)
|
113
116
|
case object
|
114
117
|
when Hash
|
115
|
-
object.transform_values {|v| duplicate(v) }
|
118
|
+
object.transform_values {|v| duplicate(v, wrapper) }
|
116
119
|
when Array
|
117
|
-
object.map {|v| duplicate(v) }
|
120
|
+
object.map {|v| duplicate(v, wrapper) }
|
118
121
|
when HexaPDF::Reference
|
119
|
-
|
122
|
+
raise HexaPDF::Error, "Import error: No source document specified" unless wrapper.source
|
123
|
+
internal_import(wrapper.source.object(object), wrapper)
|
120
124
|
when HexaPDF::Object
|
125
|
+
wrapper.source ||= object.document
|
121
126
|
if object.type == :Catalog || object.type == :Pages
|
122
127
|
@mapper[object.data] = nil
|
123
128
|
elsif (mapped_object = @mapper[object.data]&.__getobj__) && !mapped_object.null?
|
@@ -132,8 +137,8 @@ module HexaPDF
|
|
132
137
|
@destination.add(obj) if object.indirect?
|
133
138
|
|
134
139
|
obj.data.stream = obj.data.stream.dup if obj.data.stream.kind_of?(String)
|
135
|
-
obj.data.value = duplicate(obj.data.value)
|
136
|
-
obj.data.value.update(duplicate(object.copy_inherited_values)) if object.type == :Page
|
140
|
+
obj.data.value = duplicate(obj.data.value, wrapper)
|
141
|
+
obj.data.value.update(duplicate(object.copy_inherited_values, wrapper)) if object.type == :Page
|
137
142
|
obj
|
138
143
|
end
|
139
144
|
when String
|
@@ -207,7 +207,7 @@ module HexaPDF
|
|
207
207
|
@results = []
|
208
208
|
@results_item_marker_x = []
|
209
209
|
|
210
|
-
@children.
|
210
|
+
@children.each do |child|
|
211
211
|
shape = Geom2D::Polygon([left, top - height],
|
212
212
|
[left + width, top - height],
|
213
213
|
[left + width, top],
|
@@ -217,11 +217,7 @@ module HexaPDF
|
|
217
217
|
remove_indent_from_frame_shape(shape) unless shape.polygons.empty?
|
218
218
|
end
|
219
219
|
|
220
|
-
#p [:list, left, width, shape]
|
221
|
-
|
222
220
|
item_frame = Frame.new(item_frame_left, top - height, item_frame_width, height, shape: shape)
|
223
|
-
|
224
|
-
#p [index, item_frame.x, @results_item_marker_x]
|
225
221
|
@results_item_marker_x << item_frame.x - content_indentation
|
226
222
|
|
227
223
|
box_fitter = BoxFitter.new([item_frame])
|