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="")
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