class PDF::Reader::StandardKeyBuilder


* Version 4, V2 (RC4) and AESV2
* Version 1-3, all variants
This can generate a key compatible with the following standard encryption algorithms:
password and returns a key that can decrypt the file.
Processes the Encrypt dict from an encrypted PDF and a user provided

def auth_owner_pass(pass)


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

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

Used to test Owner passwords

Algorithm 7 - Authenticating the Owner Password

# 7.6.3.4 Password Algorithms
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

def auth_user_pass(pass)


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

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

Used to test User passwords

Algorithm 6 - Authenticating the User Password
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

def initialize(opts = {})

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

def key(pass)


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

Takes a string containing a user provided password.
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

def make_file_key( user_pass )

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

def pad_pass(p="")

pp61 of spec
Pads supplied password to 32bytes using PassPadBytes as specified on
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

def xor_each_byte(buf, int)

def xor_each_byte(buf, int)
  buf.each_byte.map{ |b| b^int}.pack("C*")
end