lib/hexapdf/encryption/security_handler.rb in hexapdf-0.32.2 vs lib/hexapdf/encryption/security_handler.rb in hexapdf-0.33.0

- old
+ new

@@ -45,16 +45,16 @@ # Base class for all encryption dictionaries. # # Contains entries common to all encryption dictionaries. If a specific security handler # needs further fields it should derive a new subclass and add the new fields there. # - # See: PDF1.7 s7.6.1 + # See: PDF2.0 s7.6.2 class EncryptionDictionary < Dictionary define_field :Filter, type: Symbol, required: true define_field :SubFilter, type: Symbol, version: '1.3' - define_field :V, type: Integer, required: true + define_field :V, type: Integer, required: true, allowed_values: [0, 1, 2, 3, 4, 5] define_field :Lenth, type: Integer, default: 40, version: '1.4' define_field :CF, type: Dictionary, version: '1.5' define_field :StmF, type: Symbol, default: :Identity, version: '1.5' define_field :StrF, type: Symbol, default: :Identity, version: '1.5' define_field :EFF, type: Symbol, version: '1.6' @@ -68,16 +68,12 @@ private # Ensures that the encryption dictionary's content is valid. def perform_validation super - unless [1, 2, 4, 5].include?(value[:V]) - yield("Value of /V is not one of 1, 2, 4 or 5", false) - return - end - if value[:V] == 2 && (!key?(:Length) || value[:Length] < 40 || - value[:Length] > 128 || value[:Length] % 8 != 0) + length = self[:Length] + if self[:V] == 2 && (!key?(:Length) || length < 40 || length > 128 || length % 8 != 0) yield("Invalid value for /Length field when /V is 2", false) end end end @@ -92,12 +88,12 @@ # 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. + # It is *not* recommended to create security handlers manually but only with those two methods + # listed above. # # # == Using SecurityHandler Instances # # The SecurityHandler base class provides the methods for decrypting an indirect object and for @@ -105,17 +101,21 @@ # # * #decrypt # * #encrypt_string # * #encrypt_stream # - # How the decryption/encryption key is actually computed is deferred to a sub class. + # How the decryption/encryption key is actually computed is deferred to a sub class, as per the + # PDF specification. # # 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. # + # Note that any manual changes to the encryption dictionary will invalidate the key and lead to + # an error! # + # # == Implementing a SecurityHandler Class # # Each security handler has to implement the following methods: # # prepare_encryption(**options):: @@ -149,12 +149,12 @@ attr_reader :key # The encryption algorithm. attr_reader :algorithm - # Creates a new encrypted stream data object by utilizing the given stream data object as - # template. The arguments +key+ and +algorithm+ are used for decrypting purposes. + # Creates a new encrypted stream data object by utilizing the given stream data object +obj+ + # as template. The arguments +key+ and +algorithm+ are used for decrypting purposes. def initialize(obj, key, algorithm) obj.instance_variables.each {|v| instance_variable_set(v, obj.instance_variable_get(v)) } @key = key @algorithm = algorithm end @@ -212,11 +212,11 @@ end end handler = handler.new(document) dict = document.trailer[:Encrypt] = handler.set_up_decryption(dict, **options) - HexaPDF::Object.make_direct(dict.value) + HexaPDF::Object.make_direct(dict.value, document) document.revisions.current.update(dict) document.revisions.each do |r| loader = r.loader r.loader = lambda do |xref_entry| obj = loader.call(xref_entry) @@ -262,11 +262,11 @@ end # Decrypts the strings and the possibly attached stream of the given indirect object in # place. # - # See: PDF1.7 s7.6.2 + # See: PDF2.0 s7.6.3 def decrypt(obj) return obj if @is_encrypt_dict[obj] || obj.type == :XRef key = object_key(obj.oid, obj.gen, string_algorithm) each_string_in_object(obj.value) do |str| @@ -290,20 +290,23 @@ # Returns the encrypted version of the string that resides in the given indirect object. # # Note that some strings won't be encrypted as per the specification. The returned string, # however, is always a different object. # - # See: PDF1.7 s7.6.2 + # See: PDF2.0 s7.6.3 def encrypt_string(str, obj) return str.dup 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 # Returns a Fiber that encrypts the contents of the given stream object. + # + # Note that some streams *must not be* encrypted. For those, their standard stream encoding + # fiber is returned. 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 @@ -319,12 +322,12 @@ stream_algorithm.encryption_fiber(key, result) end end 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. + # Computes the encryption key, 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: @@ -338,11 +341,11 @@ # of 40 to 128 bit or :aes for AES 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 + # See: PDF2.0 s7.6.2 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 @@ -380,13 +383,17 @@ # 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 + # See: PDF2.0 s7.6.2 def set_up_decryption(dictionary, **options) @dict = document.wrap(dictionary, type: encryption_dictionary_class) + @dict.validate do |msg, correctable, obj| + next if correctable + raise HexaPDF::Error, "Validation error for encryption dictionary (#{obj.oid},#{obj.gen}): #{msg}" + end case dict[:V] when 1, 2 strf = stmf = eff = :arc4 when 4, 5 @@ -493,11 +500,11 @@ Identity 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) + # See: PDF2.0 s7.6.3.2 (algorithm 1), PDF2.0 s7.6.3.3 (algorithm 1.A) def object_key(oid, gen, algorithm) key = encryption_key return key if dict[:V] == 5 key += [oid, gen].pack('VXv') @@ -506,16 +513,16 @@ Digest::MD5.digest(key)[0, (n_plus_5 > 16 ? 16 : n_plus_5)] 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 + # See: PDF2.0 s7.6.2 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 + when 4 then 16 # PDF2.0 s7.6.2 specifies that a /V of 4 is equal to length of 128bit + when 5 then 32 # PDF2.0 s7.6.2 specifies that a /V of 5 is equal to length of 256bit end end # Returns the class used as wrapper for the encryption dictionary. def encryption_dictionary_class