class HexaPDF::Encryption::StandardSecurityHandler
See: PDF2.0 s7.6.4
HexaPDF::Document.new needs to contain a :password key with the password.
password is supplied. To open such an encrypted PDF file, the decryption_opts
provided to
When a user or owner password is specified, a PDF file can only be opened when the correct
a user is allowed to do with a PDF file.
The access permissions (see StandardSecurityHandler::Permissions) can be used to restrict what
options when decrypting a document.
this security handler when encrypting a document. And see #prepare_decryption for all allowed
See StandardSecurityHandler::EncryptionOptions for all valid options that can be used with
permissions and a user password as well as an owner password to be set.
conforming PDF libraries and applications. This standard security handler allows access
The PDF specification defines one security handler that should be implemented by all
== Overview
/Filter value of /Standard.
The password-based standard security handler of the PDF specification, identified by a
def check_perms_field(encryption_key)
This method can only be used for revision 6.
Checks if the decrypted /Perms entry matches the /P and /EncryptMetadata entries.
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
def compute_hash(password, salt, user_key = '')
with the user password.
"#{password}#{salt}#{user_key}" where +user_key+ has to be empty when doing operations
Note: The original input (as defined by the spec) is calculated as
revision 6.
Computes a hash that is used extensively for all operations in security handlers of
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
def compute_o_field(owner_password, user_password)
method is used, otherwise the return value is incorrect!
*Attention*: If revision 6 is used, the /U value has to be computed and set before this
the /U value with added validation and key salts.
the owner password. For revision 6 the /O value is a hash computed from the password and
Short explanation: For revisions <= 4 the user password is encrypted with a key based on
Computes the encryption dictionary's /O (owner password) value.
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
def compute_oe_field(password, file_encryption_key)
the /O and /U values.
Short explanation: Encrypts the file encryption key with a key based on the password and
only).
Computes the encryption dictionary's /OE (owner encryption key) value (for revision 6
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
def compute_owner_encryption_key(password)
has to be used.
with the owner password. If the password is the user password, #compute_user_encryption_key
For revision 6 the file encryption key is a string of random bytes that has been encrypted
the owner password and then using the #compute_user_encryption_key method.
For revisions <= 4 this is done by first retrieving the user password through the use of
Computes the owner encryption key.
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
def compute_perms_field(file_encryption_key)
Uses /P and /EncryptMetadata values, so these have to be set beforehand.
Computes the encryption dictionary's /Perms (permissions) value (for revision 6 only).
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
def compute_u_field(password)
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)
password with added validation and key salts.
based on the user password. For revision 6 the /U value is a hash computed from the
Short explanation: For revisions <= 4, the password padding string is encrypted with a key
Computes the encryption dictionary's /U (user password) value.
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
def compute_ue_field(password, file_encryption_key)
the /U value.
Short explanation: Encrypts the file encryption key with a key based on the password and
only).
Computes the encryption dictionary's /UE (user encryption key) value (for revision 6
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
def compute_user_encryption_key(password)
#compute_owner_encryption_key has to be used instead.
with the user password. If the password is the owner password,
For revision 6 the file encryption key is a string of random bytes that has been encrypted
encrypt or decrypt a file.
For revisions <= 4 this is the *only* way for generating the encryption key needed to
Computes the user encryption key.
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
def decrypt(obj) #:nodoc:
def decrypt(obj) #:nodoc: if dict[:V] >= 4 && obj.type == :Metadata && obj[:Subtype] == :XML && !dict[:EncryptMetadata] obj else super end end
def encrypt_stream(obj) #:nodoc
def encrypt_stream(obj) #:nodoc if dict[:V] >= 4 && obj.type == :Metadata && obj[:Subtype] == :XML && !dict[:EncryptMetadata] obj.stream_encoder else super end end
def encryption_dictionary_class
def encryption_dictionary_class StandardEncryptionDictionary end
def encryption_key_valid?
Additionally checks that the document trailer's ID has not changed.
def encryption_key_valid? super && (document.trailer[:Encrypt][:R] > 4 || trailer_id_hash == @trailer_id_hash) end
def owner_password_valid?(password)
Authenticates the owner password, i.e. decides whether the given owner password is valid.
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
def permissions
Returns the permissions of the managed dictionary as array of symbol values.
def permissions Permissions::PERMISSION_TO_SYMBOL.each_with_object([]) do |(perm, sym), result| result << sym if dict[:P] & perm == perm end end
def prepare_decryption(password: '', check_permissions: true)
If the optional +check_permissions+ argument is +true+, the permissions for files
key.
Uses the given password (or the default password if none given) to retrieve the encryption
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]) 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 @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
def prepare_encryption(**kwoptions)
Prepares the security handler for use in encrypting the document.
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
def prepare_password(password)
See: PDF2.0 s7.6.4.3.2 (algorithm 2 step a)),
according to the PDF2.0 specification.
* For revision 6 the password is converted into UTF-8 encoding that is normalized
PASSWORD_PADDING and truncated to a maximum of 32 bytes.
* For revisions <= 4, the password is converted into ISO-8859-1 encoding, padded with
Returns the password modified so that if follows certain rules:
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
def trailer_id_hash # :nodoc:
Computes the hash value for the first string in the trailer ID array.
def trailer_id_hash # :nodoc: id = document.unwrap(document.trailer[:ID]) (id.kind_of?(Array) ? id[0] : id).hash end
def user_password_from_owner_password(owner_password)
Returns the user password when given the owner password for revisions <= 4.
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
def user_password_valid?(password)
Authenticates the user password, i.e. decides whether the given user password is valid.
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
def xor_key(key, value)
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