1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
|
# coding: utf-8
# typed: strict
require 'digest/md5'
require 'rc4'
class PDF::Reader
# 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:
#
# * Version 1-3, all variants
# * Version 4, V2 (RC4) and AESV2
#
class StandardKeyBuilder
## 7.6.3.3 Encryption Key Algorithm (pp61)
#
# needs a document's user password to build a key for decrypting an
# encrypted PDF document
#
PassPadBytes = [ 0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41,
0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08,
0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80,
0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a ] #: Array[Integer]
#: (?Hash[Symbol, untyped]) -> void
def initialize(opts = {})
@key_length = opts[:key_length].to_i/8 #: Integer
@revision = opts[:revision].to_i #: Integer
@owner_key = opts[:owner_key] #: String?
@user_key = opts[:user_key] #: String?
@permissions = opts[:permissions].to_i #: Integer
@encryptMeta = opts.fetch(:encrypted_metadata, true) #: bool
@file_id = opts[:file_id] || "" #: String
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
# 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.
#
#: (?String) -> String
def key(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
# Pads supplied password to 32bytes using PassPadBytes as specified on
# pp61 of spec
#
#: (?String?) -> String
def pad_pass(p="")
return PassPadBytes.pack('C*') if p.nil? || p.empty?
pTrimmedLength = [p.bytesize, 32].min
pad = PassPadBytes[0, 32 - pTrimmedLength] || []
p[0, 32].to_s + pad.pack('C*')
end
#: (String, Integer) -> String
def xor_each_byte(buf, int)
buf.each_byte.map{ |b| b^int}.pack("C*")
end
## 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
#
#: (String) -> String?
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|
# The RC4 gem doen't have type annotations, so the type checker doesn't
# know decrypt() returns a string
out = TypeCheck.cast_to_string!(
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
# 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
#
#: (String) -> String?
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.to_s[0, 16] == out
else
pass = RC4.new(keyBegins).encrypt(PassPadBytes.pack("C*")) == @user_key
end
pass ? keyBegins : nil
end
#: (String) -> String
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
end
end
|