Class: HexaPDF::Encryption::SecurityHandler
- Inherits:
-
Object
- Object
- HexaPDF::Encryption::SecurityHandler
- 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.
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 encrypting strings and streams:
-
#decrypt
-
#encrypt_string
-
#encrypt_stream
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)
-
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
Defined Under Namespace
Classes: EncryptedStreamData
Instance Attribute Summary collapse
-
#encryption_details ⇒ Object
readonly
A hash containing information about the used encryption.
Class Method Summary collapse
-
.set_up_decryption(document, **options) ⇒ Object
:call-seq: SecurityHandler.set_up_decryption(document, **options) -> handler.
-
.set_up_encryption(document, handler_name, **options) ⇒ Object
:call-seq: SecurityHandler.set_up_encryption(document, handler_name, **options) -> handler.
Instance Method Summary collapse
-
#decrypt(obj) ⇒ Object
Decrypts the strings and the possibly attached stream of the given indirect object in place.
-
#encrypt_stream(obj) ⇒ Object
Returns a Fiber that encrypts the contents of the given stream object.
-
#encrypt_string(str, obj) ⇒ Object
Returns the encrypted version of the string that resides in the given indirect object.
-
#encryption_key_valid? ⇒ Boolean
Checks if the encryption key computed by this security handler is derived from the document’s encryption dictionary.
-
#initialize(document) ⇒ SecurityHandler
constructor
Creates a new SecurityHandler for the given document.
-
#set_up_decryption(dictionary, **options) ⇒ Object
Uses the given encryption dictionary to set up the security handler for decrypting the document.
-
#set_up_encryption(key_length: 128, algorithm: :aes, force_v4: false, **options) ⇒ Object
Computes the encryption key, sets up the algorithms for encrypting the document based on the given options, and returns the corresponding encryption dictionary.
Constructor Details
#initialize(document) ⇒ SecurityHandler
Creates a new SecurityHandler for the given document.
249 250 251 252 253 254 255 256 257 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 249 def initialize(document) @document = document @encrypt_dict_hash = nil @encryption_details = {} @is_encrypt_dict = document.revisions.each.with_object({}) do |rev, hash| hash[rev.trailer[:Encrypt]] = true end end |
Instance Attribute Details
#encryption_details ⇒ Object (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.
246 247 248 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 246 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
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 205 def self.set_up_decryption(document, **) 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) dict = document.trailer[:Encrypt] = handler.set_up_decryption(dict, **) 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) 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).
182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 182 def self.set_up_encryption(document, handler_name, **) 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(**) 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: PDF2.0 s7.6.3
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 269 def decrypt(obj) return obj if @is_encrypt_dict[obj] || obj.type == :XRef error_proc = proc {|msg| document.config['encryption.on_decryption_error'].call(obj, msg) } 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, &error_proc)) 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, &error_proc) end obj rescue EncryptionError => e e.pdf_object = obj raise end |
#encrypt_stream(obj) ⇒ Object
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.
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 310 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 |
#encrypt_string(str, obj) ⇒ Object
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: PDF2.0 s7.6.3
298 299 300 301 302 303 304 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 298 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 |
#encryption_key_valid? ⇒ Boolean
Checks if the encryption key computed by this security handler is derived from the document’s encryption dictionary.
261 262 263 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 261 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: PDF2.0 s7.6.2
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 391 def set_up_decryption(dictionary, **) @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 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(**), 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, 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: PDF2.0 s7.6.2
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/hexapdf/encryption/security_handler.rb', line 349 def set_up_encryption(key_length: 128, algorithm: :aes, force_v4: false, **) @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] == 4 || 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, **) @encrypt_dict_hash = document.unwrap(dict).hash set_up_security_handler(*result) @dict end |