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