lib/hexapdf/encryption/standard_security_handler.rb in hexapdf-0.46.0 vs lib/hexapdf/encryption/standard_security_handler.rb in hexapdf-0.47.0

- old
+ new

@@ -104,10 +104,14 @@ # # 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. # + # **Note**: While HexaPDF supports reading files encrypted with revision 5, it doesn't support + # writing such files. This is no problem in practice since revision 5 was an inofficial Adobe + # extension to PDF 1.7 and revision 6 specified in PDF 2.0 is practically the same. + # # See: PDF2.0 s7.6.4 class StandardSecurityHandler < SecurityHandler # Defines all available permissions. # @@ -338,17 +342,17 @@ end # Uses the given password (or the default password if none given) to retrieve the encryption # key. # - # If the optional +check_permissions+ argument is +true+, the permissions for files - # encrypted with revision 6 are checked. Otherwise, permission changes are ignored. + # If the optional +check_permissions+ argument is +true+, the permissions for files encrypted + # with revision 5 or 6 are checked. Otherwise, permission changes are ignored. def prepare_decryption(password: '', check_permissions: true) if dict[:Filter] != :Standard raise(HexaPDF::UnsupportedEncryptionError, "Invalid /Filter value #{dict[:Filter]} for standard security handler") - elsif ![2, 3, 4, 6].include?(dict[:R]) + elsif ![2, 3, 4, 5, 6].include?(dict[:R]) raise(HexaPDF::UnsupportedEncryptionError, "Invalid /R value #{dict[:R]} for standard security handler") elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray) document.trailer[:ID] = ['', ''] end @@ -367,11 +371,11 @@ encryption_key = compute_owner_encryption_key(password) else raise HexaPDF::EncryptionError, "Invalid password specified" end - check_perms_field(encryption_key) if check_permissions && dict[:R] == 6 + check_perms_field(encryption_key) if check_permissions && dict[:R] >= 5 encryption_key end # Computes the hash value for the first string in the trailer ID array. @@ -394,12 +398,12 @@ # Computes the user encryption key. # # For revisions <= 4 this is the *only* way for generating the encryption key needed to # encrypt or decrypt a file. # - # 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, + # For revision 5 and 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: 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 @@ -414,30 +418,30 @@ if dict[:R] >= 3 50.times { data = Digest::MD5.digest(data[0, n]) } end data[0, n] - elsif dict[:R] == 6 + elsif dict[:R] <= 6 key = compute_hash(password, dict[:U][40, 8]) aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:UE]) end end # 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 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. + # For revisions 5 and 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.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 + elsif dict[:R] <= 6 key = compute_hash(password, dict[:O][40, 8], dict[:U]) aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:OE]) end end @@ -445,11 +449,11 @@ # # Short explanation: For revisions <= 4 the user password is encrypted with a key based on # the owner password. For revision 6 the /O value is a hash computed from the password and # 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 + # *Attention*: If revision 5 or 6 is used, the /U value has to be computed and set before this # method is used, otherwise the return value is incorrect! # # 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 @@ -463,18 +467,18 @@ if dict[:R] >= 3 19.times {|i| data = arc4_algorithm.encrypt(xor_key(key, i + 1), data) } end data - elsif dict[:R] == 6 + elsif dict[:R] <= 6 validation_salt = random_bytes(8) key_salt = random_bytes(8) compute_hash(owner_password, validation_salt, dict[:U]) << validation_salt << key_salt end end - # Computes the encryption dictionary's /OE (owner encryption key) value (for revision 6 + # Computes the encryption dictionary's /OE (owner encryption key) value (for revisions 5 and 6 # only). # # Short explanation: Encrypts the file encryption key with a key based on the password and # the /O and /U values. # @@ -485,11 +489,11 @@ end # Computes the encryption dictionary's /U (user password) value. # # 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 + # based on the user password. For revisions 5 and 6 the /U value is a hash computed from the # password with added validation and key salts. # # 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) @@ -500,18 +504,18 @@ key = compute_user_encryption_key(password) data = Digest::MD5.digest(PASSWORD_PADDING + document.trailer[:ID][0]) data = arc4_algorithm.encrypt(key, data) 19.times {|i| data = arc4_algorithm.encrypt(xor_key(key, i + 1), data) } data << "hexapdfhexapdfhe" - elsif dict[:R] == 6 + elsif dict[:R] <= 6 validation_salt = random_bytes(8) key_salt = random_bytes(8) compute_hash(password, validation_salt) << validation_salt << key_salt end end - # Computes the encryption dictionary's /UE (user encryption key) value (for revision 6 + # Computes the encryption dictionary's /UE (user encryption key) value (for revision 5 and 6 # only). # # Short explanation: Encrypts the file encryption key with a key based on the password and # the /U value. # @@ -519,11 +523,12 @@ 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). + # Computes the encryption dictionary's /Perms (permissions) value (for revisions 5 and 6 + # only). # # Uses /P and /EncryptMetadata values, so these have to be set beforehand. # # See: PDF2.0 s7.6.4.4.9 (algorithm 10) def compute_perms_field(file_encryption_key) @@ -541,29 +546,29 @@ 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] - elsif dict[:R] == 6 + elsif dict[:R] <= 6 compute_hash(password, dict[:U][32, 8]) == dict[:U][0, 32] end end # Authenticates the owner password, i.e. decides whether the given owner password is valid. # # 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 + elsif dict[:R] <= 6 compute_hash(password, dict[:O][32, 8], dict[:U]) == dict[:O][0, 32] end end # Checks if the decrypted /Perms entry matches the /P and /EncryptMetadata entries. # - # This method can only be used for revision 6. + # This method can only be used for revisions 5 and 6. # # 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" @@ -594,21 +599,22 @@ userpwd end # Computes a hash that is used extensively for all operations in security handlers of - # revision 6. + # revision 5 and 6. # # 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.4.3.4 (algorithm 2.B) + # See: PDF2.0 s7.6.4.3.4 (algorithm 2.B) and ADB Extension Level 3 s3.5.2 def compute_hash(password, salt, user_key = '') k = Digest::SHA256.digest("#{password}#{salt}#{user_key}") - e = '' + return k if dict[:R] == 5 + e = '' i = 0 while i < 64 || e.getbyte(-1) > i - 32 k1 = "#{password}#{k}#{user_key}" * 64 e = aes_algorithm.new(k[0, 16], k[16, 16], :encrypt).process(k1) k = case e.unpack('C16').inject(&:+) % 3 # 256 % 3 == 1 % 3 --> x*256 % 3 == x % 3 @@ -625,19 +631,19 @@ # Returns the password modified so that if follows certain rules: # # * For revisions <= 4, the password is converted into ISO-8859-1 encoding, padded with # PASSWORD_PADDING and truncated to a maximum of 32 bytes. # - # * For revision 6 the password is converted into UTF-8 encoding that is normalized + # * For revision 5 and 6 the password is converted into UTF-8 encoding that is normalized # according to the PDF2.0 specification. # # 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 + elsif dict[:R] <= 6 password.to_s.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)[0, 127] end rescue Encoding::UndefinedConversionError => e raise HexaPDF::EncryptionError, "Invalid character in password: #{e.error_char}" end