Class: HexaPDF::Encryption::SecurityHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/hexapdf/encryption/security_handler.rb

Overview

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:

  • #decrypt

  • #encrypt_string

  • #encrypt_stream

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.

Direct Known Subclasses

StandardSecurityHandler

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ SecurityHandler

Creates a new SecurityHandler for the given document.



215
216
217
218
219
# File 'lib/hexapdf/encryption/security_handler.rb', line 215

def initialize(document)
  @document = document
  @encrypt_dict_hash = nil
  @encryption_details = {}
end

Instance Attribute Details

#encryption_detailsObject (readonly)

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.



212
213
214
# File 'lib/hexapdf/encryption/security_handler.rb', line 212

def encryption_details
  @encryption_details
end

Class Method Details

.set_up_decryption(document, **options) ⇒ Object

:call-seq:

SecurityHandler.set_up_decryption(document, **options)   -> handler

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



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/hexapdf/encryption/security_handler.rb', line 170

def self.set_up_decryption(document, **options)
  dict = document.trailer[:Encrypt]
  if dict.nil?
    raise HexaPDF::EncryptionError, "No /Encrypt dictionary found"
  end
  handler = HexaPDF::GlobalConfiguration.constantize('encryption.filter_map', dict[:Filter])
  if handler.nil?
    handler = HexaPDF::GlobalConfiguration.constantize('encryption.sub_filter_map', dict[:SubFilter])
  end
  if handler.nil?
    raise HexaPDF::EncryptionError, "Could not find a suitable security handler"
  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

.set_up_encryption(document, handler_name, **options) ⇒ Object

:call-seq:

SecurityHandler.set_up_encryption(document, handler_name, **options)   -> handler

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).



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/hexapdf/encryption/security_handler.rb', line 145

def self.set_up_encryption(document, handler_name, **options)
  handler = HexaPDF::GlobalConfiguration.constantize('encryption.filter_map', handler_name)
  if handler.nil?
    handler = HexaPDF::GlobalConfiguration.constantize('encryption.sub_filter_map', handler_name)
  end
  if handler.nil?
    raise HexaPDF::EncryptionError, "Could not find the specified security handler"
  end

  handler = handler.new(document)
  document.trailer[:Encrypt] = handler.set_up_encryption(**options)
  handler.freeze
end

Instance Method Details

#decrypt(obj) ⇒ Object

Decrypts the strings and the possibly attached stream of the given indirect object in place.

See: PDF1.7 s7.6.2



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/hexapdf/encryption/security_handler.rb', line 231

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?
    str.replace(string_algorithm.decrypt(key, str))
  end

  if obj.kind_of?(HexaPDF::Stream)
    unless string_algorithm == stream_algorithm
      key = object_key(obj.oid, obj.gen, stream_algorithm)
    end
    obj.raw_stream.filter.unshift(:Encryption)
    obj.raw_stream.decode_parms.unshift(key: key, algorithm: stream_algorithm)
  end

  obj
end

#encrypt_stream(obj) ⇒ Object

Returns a Fiber that encrypts the contents of the given stream object.



262
263
264
265
266
267
# File 'lib/hexapdf/encryption/security_handler.rb', line 262

def encrypt_stream(obj)
  return obj.stream_encoder if obj.type == :XRef

  key = object_key(obj.oid, obj.gen, stream_algorithm)
  obj.stream_encoder(:Encryption, key: key, algorithm: stream_algorithm)
end

#encrypt_string(str, obj) ⇒ Object

Returns the encrypted version of the string that resides in the given indirect object.

See: PDF1.7 s7.6.2



254
255
256
257
258
259
# File 'lib/hexapdf/encryption/security_handler.rb', line 254

def encrypt_string(str, obj)
  return str if str.empty? || obj == document.trailer[:Encrypt] || obj.type == :XRef

  key = object_key(obj.oid, obj.gen, string_algorithm)
  string_algorithm.encrypt(key, str)
end

#encryption_key_valid?Boolean

Checks if the encryption key computed by this security handler is derived from the document’s encryption dictionary.

Returns:

  • (Boolean)


223
224
225
# File 'lib/hexapdf/encryption/security_handler.rb', line 223

def encryption_key_valid?
  document.unwrap(document.trailer[:Encrypt]).hash == @encrypt_dict_hash
end

#set_up_decryption(dictionary, **options) ⇒ Object

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



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/hexapdf/encryption/security_handler.rb', line 331

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

#set_up_encryption(key_length: 128, algorithm: :aes, force_V4: false, **options) ⇒ Object

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 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



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/hexapdf/encryption/security_handler.rb', line 289

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