class HexaPDF::Encryption::StandardSecurityHandler
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 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.
See: PDF1.7 s7.6.3, PDF2.0 s7.6.3
Constants
- PASSWORD_PADDING
The padding used for passwords with fewer than 32 bytes. Only used for revisions <= 4.
See: PDF1.7 s7.6.3.3
Public Instance Methods
HexaPDF::Encryption::SecurityHandler#encrypt_stream
# File lib/hexapdf/encryption/standard_security_handler.rb, line 250 def encrypt_stream(obj) #:nodoc if dict[:V] >= 4 && obj.type == :Metadata && obj[:Subtype] == :XML && !dict[:EncryptMetadata] obj.stream_encoder else super end end
Additionally checks that the document trailer's ID has not changed.
See: SecurityHandler#encryption_key_valid?
HexaPDF::Encryption::SecurityHandler#encryption_key_valid?
# File lib/hexapdf/encryption/standard_security_handler.rb, line 229 def encryption_key_valid? super && (document.trailer[:Encrypt][:R] > 4 || trailer_id_hash == @trailer_id_hash) end
Returns the permissions of the managed dictionary as array of symbol values.
See: Permissions
# File lib/hexapdf/encryption/standard_security_handler.rb, line 236 def permissions Permissions::PERMISSION_TO_SYMBOL.each_with_object([]) do |(perm, sym), result| result << sym if dict[:P] & perm == perm end end
Private Instance Methods
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)
# File lib/hexapdf/encryption/standard_security_handler.rb, line 542 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) raise HexaPDF::EncryptionError, "Decrypted permissions don't match /P" elsif decrypted[8] != (dict[:EncryptMetadata] ? 'T' : 'F') raise HexaPDF::EncryptionError, "Decrypted /Perms field doesn't match /EncryptMetadata" end end
Computes a hash that is used extensively for all operations in security handlers of revision 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.3.3.3 (algorithm 2.B)
# File lib/hexapdf/encryption/standard_security_handler.rb, line 581 def compute_hash(password, salt, user_key = '') k = Digest::SHA256.digest("#{password}#{salt}#{user_key}") 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 when 0 then Digest::SHA256.digest(e) when 1 then Digest::SHA384.digest(e) when 2 then Digest::SHA512.digest(e) end i += 1 end k[0, 32] end
Computes the encryption dictionary's /O (owner password) value.
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 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))
# File lib/hexapdf/encryption/standard_security_handler.rb, line 429 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) } end key = data[0, key_length] data = arc4_algorithm.encrypt(key, user_password) if dict[:R] >= 3 19.times {|i| data = arc4_algorithm.encrypt(xor_key(key, i + 1), data) } end data 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 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))
# File lib/hexapdf/encryption/standard_security_handler.rb, line 457 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
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.
See: PDF2.0 s7.6.3.3.2 (algorithm 2.A (a)-(d))
# File lib/hexapdf/encryption/standard_security_handler.rb, line 410 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]) aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:OE]) end 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)
# File lib/hexapdf/encryption/standard_security_handler.rb, line 504 def compute_perms_field(file_encryption_key) data = [dict[:P]].pack('V') data << [0xFFFFFFFF].pack('V') data << (dict[:EncryptMetadata] ? 'T' : 'F') data << 'adb' data << 'hexa' aes_algorithm.new(file_encryption_key, "\0" * 16, :encrypt).process(data) 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 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)
# File lib/hexapdf/encryption/standard_security_handler.rb, line 470 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 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 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 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))
# File lib/hexapdf/encryption/standard_security_handler.rb, line 494 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 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, 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))
# File lib/hexapdf/encryption/standard_security_handler.rb, line 379 def compute_user_encryption_key(password) if dict[:R] <= 4 data = password data += dict[:O] data << [dict[:P]].pack('V') data << document.trailer[:ID][0] data << [0xFFFFFFFF].pack('V') if dict[:R] == 4 && !dict[:EncryptMetadata] n = key_length data = Digest::MD5.digest(data) if dict[:R] >= 3 50.times { data = Digest::MD5.digest(data[0, n]) } end data[0, n] elsif dict[:R] == 6 key = compute_hash(password, dict[:U][40, 8]) aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:UE]) end end
See SecurityHandler#encryption_dictionary_class
# File lib/hexapdf/encryption/standard_security_handler.rb, line 359 def encryption_dictionary_class StandardEncryptionDictionary 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)
# File lib/hexapdf/encryption/standard_security_handler.rb, line 529 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] end 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.
# File lib/hexapdf/encryption/standard_security_handler.rb, line 322 def prepare_decryption(password: '', check_permissions: true) if dict[:Filter] != :Standard raise(HexaPDF::UnsupportedEncryptionError, "Invalid /Filter value for standard security handler") elsif ![2, 3, 4, 6].include?(dict[:R]) raise(HexaPDF::UnsupportedEncryptionError, "Invalid /R value for standard security handler") elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray) raise(HexaPDF::EncryptionError, "Document ID for needed for decryption") end @trailer_id_hash = trailer_id_hash password = prepare_password(password) if user_password_valid?(prepare_password('')) encryption_key = compute_user_encryption_key(prepare_password('')) elsif user_password_valid?(password) encryption_key = compute_user_encryption_key(password) elsif owner_password_valid?(password) 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 encryption_key end
Prepares the security handler for use in encrypting the document.
See the attributes of the EncryptionOptions
class for all possible arguments.
# File lib/hexapdf/encryption/standard_security_handler.rb, line 263 def prepare_encryption(**kwoptions) options = EncryptionOptions.new(kwoptions) dict[:Filter] = :Standard dict[:R] = case dict[:V] when 1 then 2 when 2 then 3 when 4 then 4 when 5 then 6 end dict[:EncryptMetadata] = options.encrypt_metadata dict[:P] = options.permissions if dict[:V] >= 4 cfm = if options.algorithm == :arc4 :V2 elsif key_length == 16 :AESV2 else :AESV3 end dict[:CF] = { StdCF: { CFM: cfm, AuthEvent: :DocOpen, Length: key_length, }, } dict[:StmF] = dict[:StrF] = :StdCF end if dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray) document.trailer.set_random_id end options.user_password = prepare_password(options.user_password) options.owner_password = prepare_password(options.owner_password) dict[:O] = compute_o_field(options.owner_password, options.user_password) dict[:U] = compute_u_field(options.user_password) if dict[:R] <= 4 encryption_key = compute_user_encryption_key(options.user_password) else encryption_key = random_bytes(32) dict[:UE] = compute_ue_field(options.user_password, encryption_key) dict[:OE] = compute_oe_field(options.owner_password, encryption_key) dict[:Perms] = compute_perms_field(encryption_key) end @trailer_id_hash = trailer_id_hash [encryption_key, options.algorithm, options.algorithm, options.algorithm] end
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 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))
# File lib/hexapdf/encryption/standard_security_handler.rb, line 610 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 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
Returns the user password when given the owner password for revisions <= 4.
See: PDF1.7 s7.6.3.4 (algorithm 7 (a) and (b))
# File lib/hexapdf/encryption/standard_security_handler.rb, line 556 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 key = data[0, key_length] if dict[:R] == 2 userpwd = arc4_algorithm.decrypt(key, dict[:O]) else userpwd = dict[:O] 20.times {|i| userpwd = arc4_algorithm.decrypt(xor_key(key, 19 - i), userpwd) } end userpwd 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)
# File lib/hexapdf/encryption/standard_security_handler.rb, line 516 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 compute_hash(password, dict[:U][32, 8]) == dict[:U][0, 32] end end
XORs each byte of the String key
with value and returns the resulting string.
# File lib/hexapdf/encryption/standard_security_handler.rb, line 622 def xor_key(key, value) new_key = key.dup i = 0 while i < new_key.length new_key.setbyte(i, (new_key.getbyte(i) ^ value) % 256) i += 1 end new_key end