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

- old
+ new

@@ -45,11 +45,11 @@ # # Contains additional fields that are used for storing the information needed for retrieving # the encryption key and a set of permissions. class StandardEncryptionDictionary < EncryptionDictionary - define_field :R, type: Integer, required: true + define_field :R, type: Integer, required: true, allowed_values: [2, 3, 4, 5, 6] define_field :O, type: PDFByteString, required: true define_field :OE, type: PDFByteString, version: '2.0' define_field :U, type: PDFByteString, required: true define_field :UE, type: PDFByteString, version: '2.0' define_field :P, type: Integer, required: true @@ -69,55 +69,61 @@ when 6 if !key?(:OE) || !key?(:UE) || !key?(:Perms) yield("Value of /OE, /UE or /Perms is missing for dictionary revision 6", false) return end - if value[:U].length != 48 || value[:O].length != 48 || value[:UE].length != 32 || - value[:OE].length != 32 || value[:Perms].length != 16 - yield("Invalid size for /U, /O, /UE, /OE or /Perms values for revisions 6", false) + [:U, :O].each do |f| + if value[f].length != 48 + yield("Invalid size (#{value[f].length} instead of 48) for /#{f} for revisions 6", + value[f].length > 48 && value[f][48..-1].squeeze("\x00").length == 1) + value[f].slice!(48..-1) + end end - else - yield("Value of /R is not one of 2, 3, 4 or 6", false) + if value[:UE].length != 32 || value[:OE].length != 32 || value[:Perms].length != 16 + yield("Invalid size for /UE, /OE or /Perms values for revisions 6", false) + end end end end # The password-based standard security handler of the PDF specification, identified by a # /Filter value of /Standard. # # == Overview # - # The PDF specification defines one security handler that should be implemented by all PDF - # conform libraries and applications. This standard security handler allows access permissions - # and a user password as well as an owner password to be set. See - # StandardSecurityHandler::EncryptionOptions for all valid options that can be used with this - # security handler. + # The PDF specification defines one security handler that should be implemented by all + # conforming PDF libraries and applications. This standard security handler allows access + # permissions and a user password as well as an owner password to be set. # + # See StandardSecurityHandler::EncryptionOptions for all valid options that can be used with + # this security handler when encrypting a document. And see #prepare_decryption for all allowed + # options when decrypting a document. + # # The access permissions (see StandardSecurityHandler::Permissions) can be used to restrict what # a user is allowed to do with a PDF file. # # When a user or owner password is specified, a PDF file can only be opened when the correct # password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to # HexaPDF::Document.new needs to contain a :password key with the password. # - # See: PDF1.7 s7.6.3, PDF2.0 s7.6.3 + # See: PDF2.0 s7.6.4 class StandardSecurityHandler < SecurityHandler # Defines all available permissions. # # It is possible to use an array of permission symbols instead of an integer to describe the # permission set. The used symbols are the lower case versions of the constants, i.e. the # symbol for MODIFY_CONSTANT would be :modify_constant. # - # See: PDF1.7 s7.6.3.2 + # See: PDF2.0 s7.6.4.2 module Permissions # Printing (if HIGH_QUALITY_PRINT is also set, then high quality printing is allowed) PRINT = 1 << 2 - # Modification of the content by operations that are different from those controller by + # Modification of the content by operations that are different from those controlled by # MODIFY_ANNOTATION, FILL_IN_FORMS and ASSEMBLE_DOCUMENT MODIFY_CONTENT = 1 << 3 # Copying of content COPY_CONTENT = 1 << 4 @@ -127,10 +133,13 @@ # Filling in form fields FILL_IN_FORMS = 1 << 8 # Extracting content + # + # PDF 2.0 specifies that this bit should always be set by writers and should be ignored by + # readers. Therefore this is part of the RESERVED constant. EXTRACT_CONTENT = 1 << 9 # Assembling of the document (inserting, rotating or deleting of pages and creation of # bookmarks or thumbnail images) ASSEMBLE_DOCUMENT = 1 << 10 @@ -140,12 +149,12 @@ # Allows everything ALL = PRINT | MODIFY_CONTENT | COPY_CONTENT | MODIFY_ANNOTATION | FILL_IN_FORMS | EXTRACT_CONTENT | ASSEMBLE_DOCUMENT | HIGH_QUALITY_PRINT - # Reserved permission bits - RESERVED = 0xFFFFF000 | 0b11000000 + # Reserved permission bits that should always be set + RESERVED = 0xFFFFF000 | 0b11000000 | EXTRACT_CONTENT # Maps permission symbols to their respective value SYMBOL_TO_PERMISSION = { print: PRINT, modify_content: MODIFY_CONTENT, @@ -211,11 +220,11 @@ private # Maps the permissions to an integer for use by the standard security handler. # - # See: PDF1.7 s7.6.3.2, ADB1.7 3.5.2 (table 3.20 and the paragraphs before) + # See: PDF2.0 s7.6.4.2, ADB1.7 3.5.2 (table 3.20 and the paragraphs before) def process_permissions(perms) if perms.kind_of?(Array) perms = perms.inject(0) do |result, perm| result | Permissions::SYMBOL_TO_PERMISSION.fetch(perm, 0) end @@ -361,11 +370,11 @@ StandardEncryptionDictionary end # The padding used for passwords with fewer than 32 bytes. Only used for revisions <= 4. # - # See: PDF1.7 s7.6.3.3 + # See: PDF2.0 s7.6.4.3 PASSWORD_PADDING = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08" \ "\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A".b # Computes the user encryption key. # @@ -374,11 +383,11 @@ # # For revision 6 the file encryption key is a string of random bytes that has been encrypted # with the user password. If the password is the owner password, # #compute_owner_encryption_key has to be used instead. # - # See: PDF1.7 s7.6.3.3 (algorithm 2), PDF2.0 s7.6.3.3.2 (algorithm 2.A (a)-(b),(e)) + # See: PDF2.0 s7.6.4.3.2 (algorithm 2), PDF2.0 s7.6.4.3.3 (algorithm 2.A (a)-(b),(e)) def compute_user_encryption_key(password) if dict[:R] <= 4 data = password data += dict[:O] data << [dict[:P]].pack('V') @@ -401,15 +410,15 @@ # Computes the owner encryption key. # # For revisions <= 4 this is done by first retrieving the user password through the use of # the owner password and then using the #compute_user_encryption_key method. # - # For revision 6 file encryption key is a string of random bytes that has been encrypted - # with the owner password. If the password is the user password, - # #compute_user_encryption_key has to be used. + # For revision 6 the file encryption key is a string of random bytes that has been encrypted + # with the owner password. If the password is the user password, #compute_user_encryption_key + # has to be used. # - # See: PDF2.0 s7.6.3.3.2 (algorithm 2.A (a)-(d)) + # See: PDF2.0 s7.6.4.3.2 (algorithm 2.A (a)-(d)) def compute_owner_encryption_key(password) if dict[:R] <= 4 compute_user_encryption_key(user_password_from_owner_password(password)) elsif dict[:R] == 6 key = compute_hash(password, dict[:O][40, 8], dict[:U]) @@ -424,11 +433,11 @@ # the /U value with added validation and key salts. # # *Attention*: If revision 6 is used, the /U value has to be computed and set before this # method is used, otherwise the return value is incorrect! # - # See: PDF1.7 s7.6.3.4 (algorithm 3), PDF2.0 s7.6.3.4.7 (algorithm 9 (a)) + # See: PDF2.0 s7.6.4.4.2 (algorithm 3), PDF2.0 s7.6.4.4.8 (algorithm 9 (a)) def compute_o_field(owner_password, user_password) if dict[:R] <= 4 data = Digest::MD5.digest(owner_password) if dict[:R] >= 3 50.times { data = Digest::MD5.digest(data) } @@ -452,11 +461,11 @@ # only). # # Short explanation: Encrypts the file encryption key with a key based on the password and # the /O and /U values. # - # See: PDF2.0 s7.6.3.4.7 (algorithm 9 (b)) + # See: PDF2.0 s7.6.4.4.8 (algorithm 9 (b)) def compute_oe_field(password, file_encryption_key) key = compute_hash(password, dict[:O][40, 8], dict[:U]) aes_algorithm.new(key, "\0" * 16, :encrypt).process(file_encryption_key) end @@ -464,12 +473,12 @@ # # Short explanation: For revisions <= 4, the password padding string is encrypted with a key # based on the user password. For revision 6 the /U value is a hash computed from the # password with added validation and key salts. # - # See: PDF1.7 s7.6.3.4 (algorithm 4 for R=2, algorithm 5 for R=3 and R=4) - # PDF2.0 s7.6.3.4.6 (algorithm 8 (a) for R=6) + # See: PDF2.0 s7.6.4.4.3 (algorithm 4 for R=2), PDF s7.6.4.4.4 (algorithm 5 for R=3 and R=4) + # PDF2.0 s7.6.4.4.7 (algorithm 8 (a) for R=6) def compute_u_field(password) if dict[:R] == 2 key = compute_user_encryption_key(password) arc4_algorithm.encrypt(key, PASSWORD_PADDING) elsif dict[:R] <= 4 @@ -489,21 +498,21 @@ # only). # # Short explanation: Encrypts the file encryption key with a key based on the password and # the /U value. # - # See: PDF2.0 s7.6.3.4.6 (algorithm 8 (b)) + # See: PDF2.0 s7.6.4.4.7 (algorithm 8 (b)) def compute_ue_field(password, file_encryption_key) key = compute_hash(password, dict[:U][40, 8]) aes_algorithm.new(key, "\0" * 16, :encrypt).process(file_encryption_key) end # Computes the encryption dictionary's /Perms (permissions) value (for revision 6 only). # # Uses /P and /EncryptMetadata values, so these have to be set beforehand. # - # See: PDF2.0 s7.6.3.4.8 (algorithm 10) + # See: PDF2.0 s7.6.4.4.9 (algorithm 10) def compute_perms_field(file_encryption_key) data = [dict[:P]].pack('V') data << [0xFFFFFFFF].pack('V') data << (dict[:EncryptMetadata] ? 'T' : 'F') data << 'adb' @@ -511,11 +520,11 @@ aes_algorithm.new(file_encryption_key, "\0" * 16, :encrypt).process(data) end # Authenticates the user password, i.e. decides whether the given user password is valid. # - # See: PDF1.7 s7.6.3.4 (algorithm 6), PDF2.0 s7.6.3.4.9 (algorithm 11) + # See: PDF2.0 s7.6.4.4.5 (algorithm 6), PDF2.0 s7.6.4.4.10 (algorithm 11) def user_password_valid?(password) if dict[:R] == 2 compute_u_field(password) == dict[:U] elsif dict[:R] <= 4 compute_u_field(password)[0, 16] == dict[:U][0, 16] @@ -524,11 +533,11 @@ end end # Authenticates the owner password, i.e. decides whether the given owner password is valid. # - # See: PDF1.7 s7.6.3.4 (algorithm 7), PDF2.0 s7.6.3.4.10 (algorithm 12) + # See: PDF2.0 s7.6.4.4.6 (algorithm 7), PDF2.0 s7.6.4.4.11 (algorithm 12) def owner_password_valid?(password) if dict[:R] <= 4 user_password_valid?(user_password_from_owner_password(password)) elsif dict[:R] == 6 compute_hash(password, dict[:O][32, 8], dict[:U]) == dict[:O][0, 32] @@ -537,11 +546,11 @@ # Checks if the decrypted /Perms entry matches the /P and /EncryptMetadata entries. # # This method can only be used for revision 6. # - # See: PDF2.0 s7.6.3.4.11 (algorithm 13) + # See: PDF2.0 s7.6.4.4.12 (algorithm 13) def check_perms_field(encryption_key) decrypted = aes_algorithm.new(encryption_key, "\0" * 16, :decrypt).process(dict[:Perms]) if decrypted[9, 3] != "adb" raise HexaPDF::EncryptionError, "/Perms field cannot be decrypted" elsif (dict[:P] & 0xFFFFFFFF) != (decrypted[0, 4].unpack1('V') & 0xFFFFFFFF) @@ -551,11 +560,11 @@ end end # Returns the user password when given the owner password for revisions <= 4. # - # See: PDF1.7 s7.6.3.4 (algorithm 7 (a) and (b)) + # See: PDF2.0 s7.6.4.4.6 (algorithm 7 (a) and (b)) def user_password_from_owner_password(owner_password) data = Digest::MD5.digest(owner_password) if dict[:R] >= 3 50.times { data = Digest::MD5.digest(data) } end @@ -576,11 +585,11 @@ # # Note: The original input (as defined by the spec) is calculated as # "#{password}#{salt}#{user_key}" where +user_key+ has to be empty when doing operations # with the user password. # - # See: PDF2.0 s7.6.3.3.3 (algorithm 2.B) + # See: PDF2.0 s7.6.4.3.4 (algorithm 2.B) def compute_hash(password, salt, user_key = '') k = Digest::SHA256.digest("#{password}#{salt}#{user_key}") e = '' i = 0 @@ -604,11 +613,11 @@ # PASSWORD_PADDING and truncated to a maximum of 32 bytes. # # * For revision 6 the password is converted into UTF-8 encoding that is normalized # according to the PDF2.0 specification. # - # See: PDF1.7 s7.6.3.3 (algorithm 2 step a)), - # PDF2.0 s7.6.3.3.2 (algorithm 2.A steps a) and b)) + # See: PDF2.0 s7.6.4.3.2 (algorithm 2 step a)), + # PDF2.0 s7.6.4.3.3 (algorithm 2.A steps a) and b)) def prepare_password(password) if dict[:R] <= 4 password.to_s[0, 32].encode(Encoding::ISO_8859_1).force_encoding(Encoding::BINARY). ljust(32, PASSWORD_PADDING) elsif dict[:R] == 6