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