class PDF::Reader::StandardKeyBuilder

Processes the Encrypt dict from an encrypted PDF and a user provided password and returns a key that can decrypt the file.

This can generate a key compatible with the following standard encryption algorithms:

Constants

PassPadBytes
7.6.3.3 Encryption Key Algorithm (pp61)

needs a document’s user password to build a key for decrypting an encrypted PDF document

Public Class Methods

new(opts = {}) click to toggle source
# File lib/pdf/reader/standard_key_builder.rb, line 28
def initialize(opts = {})
  @key_length    = opts[:key_length].to_i/8
  @revision      = opts[:revision].to_i
  @owner_key     = opts[:owner_key]
  @user_key      = opts[:user_key]
  @permissions   = opts[:permissions].to_i
  @encryptMeta   = opts.fetch(:encrypted_metadata, true)
  @file_id       = opts[:file_id] || ""

  if @key_length != 5 && @key_length != 16
    msg = "StandardKeyBuilder only supports 40 and 128 bit\
           encryption (#{@key_length * 8}bit)"
    raise UnsupportedFeatureError, msg
  end
end

Public Instance Methods

key(pass) click to toggle source

Takes a string containing a user provided password.

If the password matches the file, then a string containing a key suitable for decrypting the file will be returned. If the password doesn’t match the file, and exception will be raised.

# File lib/pdf/reader/standard_key_builder.rb, line 50
def key(pass)
  pass ||= ""
  encrypt_key   = auth_owner_pass(pass)
  encrypt_key ||= auth_user_pass(pass)

  raise PDF::Reader::EncryptedPDFError, "Invalid password (#{pass})" if encrypt_key.nil?
  encrypt_key
end

Private Instance Methods

auth_owner_pass(pass) click to toggle source
7.6.3.4 Password Algorithms

Algorithm 7 - Authenticating the Owner Password

Used to test Owner passwords

if the string is a valid owner password this will return the user password that should be used to decrypt the document.

if the supplied password is not a valid owner password for this document then it returns nil

# File lib/pdf/reader/standard_key_builder.rb, line 87
def auth_owner_pass(pass)
  md5 = Digest::MD5.digest(pad_pass(pass))
  if @revision > 2 then
    50.times { md5 = Digest::MD5.digest(md5) }
    keyBegins = md5[0, @key_length]
    #first iteration decrypt owner_key
    out = @owner_key
    #RC4 keyed with (keyBegins XOR with iteration #) to decrypt previous out
    19.downto(0).each { |i| out=RC4.new(xor_each_byte(keyBegins,i)).decrypt(out) }
  else
    out = RC4.new( md5[0, 5] ).decrypt( @owner_key )
  end
  # c) check output as user password
  auth_user_pass( out )
end
auth_user_pass(pass) click to toggle source

Algorithm 6 - Authenticating the User Password

Used to test User passwords

if the string is a valid user password this will return the user password that should be used to decrypt the document.

if the supplied password is not a valid user password for this document then it returns nil

# File lib/pdf/reader/standard_key_builder.rb, line 113
def auth_user_pass(pass)
  keyBegins = make_file_key(pass)
  if @revision >= 3
    #initialize out for first iteration
    out = Digest::MD5.digest(PassPadBytes.pack("C*") + @file_id)
    #zero doesn't matter -> so from 0-19
    20.times{ |i| out=RC4.new(xor_each_byte(keyBegins, i)).encrypt(out) }
    pass = @user_key[0, 16] == out
  else
    pass = RC4.new(keyBegins).encrypt(PassPadBytes.pack("C*")) == @user_key
  end
  pass ? keyBegins : nil
end
make_file_key( user_pass ) click to toggle source
# File lib/pdf/reader/standard_key_builder.rb, line 127
def make_file_key( user_pass )
  # a) if there's a password, pad it to 32 bytes, else, just use the padding.
  @buf  = pad_pass(user_pass)
  # c) add owner key
  @buf << @owner_key
  # d) add permissions 1 byte at a time, in little-endian order
  (0..24).step(8){|e| @buf << (@permissions >> e & 0xFF)}
  # e) add the file ID
  @buf << @file_id
  # f) if revision >= 4 and metadata not encrypted then add 4 bytes of 0xFF
  if @revision >= 4 && !@encryptMeta
    @buf << [0xFF,0xFF,0xFF,0xFF].pack('C*')
  end
  # b) init MD5 digest + g) finish the hash
  md5 = Digest::MD5.digest(@buf)
  # h) spin hash 50 times
  if @revision >= 3
    50.times {
      md5 = Digest::MD5.digest(md5[0, @key_length])
    }
  end
  # i) n = key_length revision >= 3, n = 5 revision == 2
  if @revision < 3
    md5[0, 5]
  else
    md5[0, @key_length]
  end
end
pad_pass(p="") click to toggle source

Pads supplied password to 32bytes using PassPadBytes as specified on pp61 of spec

# File lib/pdf/reader/standard_key_builder.rb, line 63
def pad_pass(p="")
  if p.nil? || p.empty?
    PassPadBytes.pack('C*')
  else
    p[0, 32] + PassPadBytes[0, 32-p.length].pack('C*')
  end
end
xor_each_byte(buf, int) click to toggle source
# File lib/pdf/reader/standard_key_builder.rb, line 71
def xor_each_byte(buf, int)
  buf.each_byte.map{ |b| b^int}.pack("C*")
end