class HexaPDF::Encryption::SecurityHandler
Base class for all security handlers.
Creating SecurityHandler
Instances¶ ↑
The base class provides two class methods for this:
-
The method
::set_up_encryption
is used when a security handler instance should be created that populates the document's encryption dictionary. -
The method
::set_up_decryption
is used when a security handler should be created from the document's encryption dictionary.
Security handlers could also be created with the ::new
method but this is discouraged because the above methods provide the correct handling in both cases.
Using SecurityHandler
Instances¶ ↑
The SecurityHandler
base class provides the methods for decrypting an indirect object and for encrypting strings and streams:
How the decryption/encryption key is actually computed is deferred to a sub class.
Additionally, the encryption_key_valid?
method can be used to check whether the SecurityHandler
instance is built from/built for the current version of the encryption dictionary.
Implementing a SecurityHandler
Class¶ ↑
Each security handler has to implement the following methods:
- prepare_encryption(**options)
-
Prepares the security handler for use in encrypting the document.
See the
set_up_encryption
documentation for information on which options are passed on to this method.Returns the encryption key as well as the names of the string, stream and embedded file algorithms.
- prepare_decryption(**options)
-
Prepares the security handler for decryption by using the information from the document's encryption dictionary as well as the provided arguments.
See the
set_up_decryption
documentation for additional information.Returns the encryption key that should be used for decryption.
Additionally, the following methods can be overridden to provide a more specific meaning:
encryption_dictionary_class
-
Returns the class that is used for the encryption dictionary. Should be derived from the
EncryptionDictionary
class.
Attributes
A hash containing information about the used encryption. This information is only available once the security handler has been set up for decryption or encryption.
Available keys:
- :version
-
The version of the security handler in use.
- :string_algorithm
-
The algorithm used for encrypting/decrypting strings.
- :stream_algorithm
-
The algorithm used for encrypting/decrypting streams.
- :embedded_file_algorithm
-
The algorithm used for encrypting/decrypting embedded files.
- :key_length
-
The key length in bits.
Public Class Methods
Creates a new SecurityHandler
for the given document.
# File lib/hexapdf/encryption/security_handler.rb, line 245 def initialize(document) @document = document @encrypt_dict_hash = nil @encryption_details = {} end
Sets up and returns the security handler that is used for decrypting the given document and modifies the document's object loader so that the decryption is handled automatically behind the scenes.
The decryption_opts
has to contain decryption options specific to the security handler that is used by the PDF file.
See: set_up_decryption
# File lib/hexapdf/encryption/security_handler.rb, line 203 def self.set_up_decryption(document, **options) dict = document.trailer[:Encrypt] if dict.nil? raise HexaPDF::EncryptionError, "No /Encrypt dictionary found" end handler = document.config.constantize('encryption.filter_map', dict[:Filter]) do document.config.constantize('encryption.sub_filter_map', dict[:SubFilter]) do raise HexaPDF::EncryptionError, "Could not find a suitable security handler" end end handler = handler.new(document) document.trailer[:Encrypt] = handler.set_up_decryption(dict, **options) document.revisions.each do |r| loader = r.loader r.loader = lambda do |xref_entry| obj = loader.call(xref_entry) xref_entry.compressed? ? obj : handler.decrypt(obj) end end handler.freeze end
Sets up and returns the security handler with the specified name for the document and modifies then document's encryption dictionary accordingly.
The encryption_opts
can contain any encryption options for the specific security handler and the common encryption options.
See: set_up_encryption
(for the common encryption options).
# File lib/hexapdf/encryption/security_handler.rb, line 180 def self.set_up_encryption(document, handler_name, **options) handler = document.config.constantize('encryption.filter_map', handler_name) do document.config.constantize('encryption.sub_filter_map', handler_name) do raise HexaPDF::EncryptionError, "Could not find the specified security handler" end end handler = handler.new(document) document.trailer[:Encrypt] = handler.set_up_encryption(**options) handler.freeze end
Public Instance Methods
Decrypts the strings and the possibly attached stream of the given indirect object in place.
See: PDF1.7 s7.6.2
# File lib/hexapdf/encryption/security_handler.rb, line 261 def decrypt(obj) return obj if obj == document.trailer[:Encrypt] || obj.type == :XRef key = object_key(obj.oid, obj.gen, string_algorithm) each_string_in_object(obj.value) do |str| next if str.empty? || (obj.type == :Sig && obj[:Contents].equal?(str)) str.replace(string_algorithm.decrypt(key, str)) end if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream.filter[0] != :Crypt unless string_algorithm == stream_algorithm key = object_key(obj.oid, obj.gen, stream_algorithm) end obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm) end obj end
Returns a Fiber that encrypts the contents of the given stream object.
# File lib/hexapdf/encryption/security_handler.rb, line 292 def encrypt_stream(obj) return obj.stream_encoder if obj.type == :XRef key = object_key(obj.oid, obj.gen, stream_algorithm) source = obj.stream_source result = obj.stream_encoder(source) if result == source && obj.raw_stream.kind_of?(EncryptedStreamData) && obj.raw_stream.key == key && obj.raw_stream.algorithm == stream_algorithm obj.raw_stream.undecrypted_fiber else filter = obj[:Filter] if filter == :Crypt || (filter.kind_of?(PDFArray) && filter[0] == :Crypt) result else stream_algorithm.encryption_fiber(key, result) end end end
Returns the encrypted version of the string that resides in the given indirect object.
See: PDF1.7 s7.6.2
# File lib/hexapdf/encryption/security_handler.rb, line 283 def encrypt_string(str, obj) return str if str.empty? || obj == document.trailer[:Encrypt] || obj.type == :XRef || (obj.type == :Sig && obj[:Contents].equal?(str)) key = object_key(obj.oid, obj.gen, string_algorithm) string_algorithm.encrypt(key, str) end
Checks if the encryption key computed by this security handler is derived from the document's encryption dictionary.
# File lib/hexapdf/encryption/security_handler.rb, line 253 def encryption_key_valid? document.unwrap(document.trailer[:Encrypt]).hash == @encrypt_dict_hash end
Uses the given encryption dictionary to set up the security handler for decrypting the document.
The security handler specific options
are passed on to the prepare_decryption method.
See: PDF1.7 s7.6.1, PDF2.0 s7.6.1
# File lib/hexapdf/encryption/security_handler.rb, line 373 def set_up_decryption(dictionary, **options) @dict = document.wrap(dictionary, type: encryption_dictionary_class) case dict[:V] when 1, 2 strf = stmf = eff = :arc4 when 4, 5 strf, stmf, eff = [:StrF, :StmF, :EFF].map do |alg| if dict[:CF] && (cf_dict = dict[:CF][dict[alg]]) case cf_dict[:CFM] when :V2 then :arc4 when :AESV2, :AESV3 then :aes when :None then :identity else raise(HexaPDF::UnsupportedEncryptionError, "Unsupported encryption method: #{cf_dict[:CFM]}") end else :identity end end eff = stmf unless dict[:EFF] else raise HexaPDF::UnsupportedEncryptionError, "Unsupported encryption version #{dict[:V]}" end set_up_security_handler(prepare_decryption(**options), strf, stmf, eff) @encrypt_dict_hash = document.unwrap(@dict).hash @dict end
Computes the encryption key and sets up the algorithms for encrypting the document based on the given options, and returns the corresponding encryption dictionary.
The security handler specific options
as well as the algorithm
argument are passed on to the prepare_encryption method.
Options for all security handlers:
key_length
-
The key length in bits. Possible values are in the range of 40 to 128 and 256 and it needs to be divisible by 8.
- algorithm
-
The encryption algorithm. Possible values are :arc4 for
ARC4
encryption with key lengths of 40 to 128 bit or :aes forAES
encryption with key lengths of 128 or 256 bit. - force_v4
-
Forces the use of protocol version 4 when
key_length
=128 and algorithm=:arc4.
See: PDF1.7 s7.6.1, PDF2.0 s7.6.1
# File lib/hexapdf/encryption/security_handler.rb, line 331 def set_up_encryption(key_length: 128, algorithm: :aes, force_v4: false, **options) @dict = document.wrap({}, type: encryption_dictionary_class) dict[:V] = case key_length when 40 1 when 48, 56, 64, 72, 80, 88, 96, 104, 112, 120 2 when 128 (algorithm == :aes || force_v4 ? 4 : 2) when 256 5 else raise(HexaPDF::UnsupportedEncryptionError, "Invalid key length #{key_length} specified") end dict[:Length] = key_length if dict[:V] == 2 if ![:aes, :arc4].include?(algorithm) raise(HexaPDF::UnsupportedEncryptionError, "Unsupported encryption algorithm: #{algorithm}") elsif key_length < 128 && algorithm == :aes raise(HexaPDF::UnsupportedEncryptionError, "AES algorithm needs a key length of 128 or 256 bit") elsif key_length == 256 && algorithm == :arc4 raise(HexaPDF::UnsupportedEncryptionError, "ARC4 algorithm can only be used with key lengths between 40 and 128 bit") end result = prepare_encryption(algorithm: algorithm, **options) @encrypt_dict_hash = document.unwrap(dict).hash set_up_security_handler(*result) @dict end
Private Instance Methods
Returns the class that is used for AES
encryption.
# File lib/hexapdf/encryption/security_handler.rb, line 473 def aes_algorithm @aes_algorithm ||= document.config.constantize('encryption.aes') end
Returns the class that is used for ARC4
encryption.
# File lib/hexapdf/encryption/security_handler.rb, line 468 def arc4_algorithm @arc4_algorithm ||= document.config.constantize('encryption.arc4') end
Returns the encryption dictionary used by this security handler.
Subclasses should use this dictionary to read and set values.
# File lib/hexapdf/encryption/security_handler.rb, line 417 def dict @dict end
Returns the associated PDF document.
Subclasses should use this method to access the document.
# File lib/hexapdf/encryption/security_handler.rb, line 410 def document @document end
Finds all strings in the given object and yields them.
Note: Decryption happens directly after parsing and loading an object, before it can be touched by anthing else. Therefore we only have to contend with the basic data structures.
# File lib/hexapdf/encryption/security_handler.rb, line 522 def each_string_in_object(obj, &block) # :yields: str case obj when Hash obj.each_value {|val| each_string_in_object(val, &block) } when Array obj.each {|inner_o| each_string_in_object(inner_o, &block) } when String yield(obj) end end
Returns the algorithm class that is used for encrypting/decrypting embedded files.
Only available after decryption or encryption has been set up.
# File lib/hexapdf/encryption/security_handler.rb, line 445 def embedded_file_algorithm @embedded_file_algorithm end
Returns the class used as wrapper for the encryption dictionary.
# File lib/hexapdf/encryption/security_handler.rb, line 509 def encryption_dictionary_class EncryptionDictionary end
Returns the encryption key that is used for encryption/decryption.
Only available after decryption or encryption has been set up.
# File lib/hexapdf/encryption/security_handler.rb, line 424 def encryption_key @encryption_key end
Returns the class that is used for the identity algorithm which passes back the data as is without encrypting or decrypting it.
# File lib/hexapdf/encryption/security_handler.rb, line 479 def identity_algorithm Identity end
Returns the length of the encryption key in bytes based on the security handlers version.
See: PDF1.7 s7.6.1, PDF2.0 s7.6.1
# File lib/hexapdf/encryption/security_handler.rb, line 499 def key_length case dict[:V] when 1 then 5 when 2 then dict[:Length] / 8 when 4 then 16 # PDF2.0 s7.6.1 specifies that a /V of 4 is equal to length of 128bit when 5 then 32 # PDF2.0 s7.6.1 specifies that a /V of 5 is equal to length of 256bit end end
Computes the key for decrypting the indirect object with the given algorithm.
See: PDF1.7 s7.6.2 (algorithm 1), PDF2.0 s7.6.2.2 (algorithm 1.A)
# File lib/hexapdf/encryption/security_handler.rb, line 486 def object_key(oid, gen, algorithm) key = encryption_key return key if dict[:V] == 5 key += [oid, gen].pack('VXv') key << "sAlT" if algorithm.ancestors.include?(AES) n_plus_5 = key_length + 5 Digest::MD5.digest(key)[0, (n_plus_5 > 16 ? 16 : n_plus_5)] end
Returns n
random bytes.
# File lib/hexapdf/encryption/security_handler.rb, line 514 def random_bytes(n) aes_algorithm.random_bytes(n) end
Assigns all necessary attributes so that encryption/decryption works correctly.
The assigned values can be retrieved via the encryption_key
, string_algorithm
, stream_algorithm
and embedded_file_algorithm
methods.
# File lib/hexapdf/encryption/security_handler.rb, line 453 def set_up_security_handler(key, strf, stmf, eff) @encryption_key = key @string_algorithm = send("#{strf}_algorithm") @stream_algorithm = send("#{stmf}_algorithm") @embedded_file_algorithm = send("#{eff}_algorithm") @encryption_details = { version: dict[:V], string_algorithm: strf, stream_algorithm: stmf, embedded_file_algorithm: eff, key_length: key_length * 8, } end
Returns the algorithm class that is used for encrypting/decrypting streams.
Only available after decryption or encryption has been set up.
# File lib/hexapdf/encryption/security_handler.rb, line 438 def stream_algorithm @stream_algorithm end
Returns the algorithm class that is used for encrypting/decrypting strings.
Only available after decryption or encryption has been set up.
# File lib/hexapdf/encryption/security_handler.rb, line 431 def string_algorithm @string_algorithm end