Class: Pdfrb::Encryption::StandardSecurityHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/pdfrb/encryption/standard_security_handler.rb

Constant Summary collapse

PAD =
String.new("\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",
encoding: Encoding::BINARY).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(trailer_dict, precomputed_key: nil) ⇒ StandardSecurityHandler

Returns a new instance of StandardSecurityHandler.

Parameters:

  • trailer_dict (Hash)

    must contain :Encrypt (the /Encrypt dict Hash) and optionally :ID (the trailer /ID array).

  • precomputed_key (String, nil) (defaults to: nil)

    when supplied, skips password-based key derivation and uses this raw key. Used by .for_v5 to inject the freshly-generated file encryption key.



20
21
22
23
24
25
26
27
28
29
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 20

def initialize(trailer_dict, precomputed_key: nil)
  @encrypt = trailer_dict[:Encrypt]
  @encrypt_dict = @encrypt
  @id = trailer_dict[:ID]
  @version = (@encrypt[:V] || 0).to_i
  @revision = (@encrypt[:R] || 3).to_i
  @key_length = (@encrypt[:Length] || 40).to_i / 8
  @permissions = (@encrypt[:P] || -1).to_i
  @key = precomputed_key
end

Instance Attribute Details

#encrypt_dictObject (readonly)

Returns the value of attribute encrypt_dict.



13
14
15
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13

def encrypt_dict
  @encrypt_dict
end

#keyObject (readonly)

Returns the value of attribute key.



13
14
15
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13

def key
  @key
end

#key_lengthObject (readonly)

Returns the value of attribute key_length.



13
14
15
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13

def key_length
  @key_length
end

#permissionsObject (readonly)

Returns the value of attribute permissions.



13
14
15
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13

def permissions
  @permissions
end

#revisionObject (readonly)

Returns the value of attribute revision.



13
14
15
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13

def revision
  @revision
end

#versionObject (readonly)

Returns the value of attribute version.



13
14
15
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13

def version
  @version
end

Class Method Details

.for_v5(user_password:, owner_password:, permissions: -1,, id: SecureRandom.hex(16).b, key_length: 32) ⇒ Object

Construct a write-side V5 (AES-256, R6) handler from a user and owner password. Returns a StandardSecurityHandler whose encrypt_dict is populated with /U, /O, /UE, /OE, /Perms ready for inclusion in the trailer. The handler's key is set to the freshly-generated file encryption key so #encrypt works.



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
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 36

def self.for_v5(user_password:, owner_password:, permissions: -1,
                id: SecureRandom.hex(16).b, key_length: 32)
  file_key = SecureRandom.random_bytes(key_length)

  u_validation_salt = V5Writer.random_salt
  u_key_salt = V5Writer.random_salt
  o_validation_salt = V5Writer.random_salt
  o_key_salt = V5Writer.random_salt

  u_entry = V5Writer.build_u_entry(password: user_password.to_s,
                                   validation_salt: u_validation_salt,
                                   key_salt: u_key_salt)
  o_entry = V5Writer.build_o_entry(owner_password: owner_password.to_s,
                                   user_password: user_password.to_s,
                                   validation_salt: o_validation_salt,
                                   key_salt: o_key_salt)

  u_intermediate = V5Writer.hash_v5(user_password.to_s, u_key_salt)
  ue = V5Writer.aes_ecb_encrypt(file_key, u_intermediate)

  o_intermediate = V5Writer.hash_v5(owner_password.to_s + user_password.to_s, o_key_salt)
  oe = V5Writer.aes_ecb_encrypt(file_key, o_intermediate)

  perms_plain = "#{[permissions & 0xFFFFFFFF, 0xFFFFFFFF].pack('VV')}T#{SecureRandom.random_bytes(7)}"

  encrypt_dict = {
    Filter: :Standard,
    V: 5,
    R: 6,
    Length: key_length * 8,
    P: permissions,
    U: u_entry,
    O: o_entry,
    UE: ue,
    OE: oe,
    Perms: V5Writer.aes_ecb_encrypt(perms_plain, file_key),
  }

  trailer = { Encrypt: encrypt_dict, ID: [id, id] }
  new(trailer, precomputed_key: file_key)
end

Instance Method Details

#compute_encryption_key(password) ⇒ Object



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
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 101

def compute_encryption_key(password)
  return compute_encryption_key_v5(password) if @version >= 5

  padded = pad_password(password)
  owner_hash = @encrypt[:O] || String.new("", encoding: Encoding::BINARY)
  perms = [@permissions].pack("V")
  id_first = id_bytes

  md5 = Digest::MD5.new
  md5.update(padded)
  md5.update(owner_hash)
  md5.update(perms)
  md5.update(id_first)

  if @revision >= 4
    md5.update(@encrypt[:UE] || "")
    md5.update(@encrypt[:Perms] ? [@encrypt[:Perms]].pack("N") : "")
  end

  hash = md5.digest

  if @revision >= 3
    50.times { hash = Digest::MD5.digest(hash[0, @key_length]) }
  end

  hash[0, @key_length]
end

#compute_encryption_key_v5(password) ⇒ Object

V5 key derivation: SHA-256 of (password + key_salt) gives an intermediate hash. AES-ECB-decrypt /UE with the intermediate to recover the file encryption key.



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 132

def compute_encryption_key_v5(password)
  u_entry = @encrypt[:U] || "".b
  _hash, _vsalt, key_salt = V5Writer.extract_v5_salts(u_entry)
  return nil unless key_salt

  intermediate = V5Writer.hash_v5(password.to_s, key_salt)
  ue = @encrypt[:UE] || "".b
  return nil if ue.bytesize != 32

  V5Writer.aes_ecb_decrypt(ue, intermediate)
end

#compute_object_key(oid, gen) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 144

def compute_object_key(oid, gen)
  return compute_object_key_v5(oid, gen) if @version >= 5

  md5 = Digest::MD5.new
  md5.update(@key)
  md5.update([oid].pack("V"))
  md5.update([gen].pack("v"))

  if @version >= 4
    md5.update("\xAA")
  end

  md5.digest[0, [@key_length + 5, 16].min]
end

#compute_object_key_v5(oid, gen) ⇒ Object

V5 per-object key: SHA-256 of (file_key || oid_le32 || gen_le32).



160
161
162
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 160

def compute_object_key_v5(oid, gen)
  Digest::SHA256.digest(@key + [oid].pack("V") + [gen].pack("V"))
end

#decrypt_data(data, oid, gen) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 181

def decrypt_data(data, oid, gen)
  return data unless @key

  obj_key = compute_object_key(oid, gen)

  if @version >= 4
    decrypt_aes_cbc(data, obj_key)
  else
    decrypt_rc4(data, obj_key)
  end
end

#encrypt_data(data, oid, gen) ⇒ Object Also known as: encrypt



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 164

def encrypt_data(data, oid, gen)
  return data unless @key

  obj_key = compute_object_key(oid, gen)

  if @version >= 4
    encrypt_aes_cbc(data, obj_key)
  else
    encrypt_rc4(data, obj_key)
  end
end

#verify_user_password?(password) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 78

def verify_user_password?(password)
  return verify_user_password_v5?(password) if @version >= 5

  computed = compute_encryption_key(password)
  @key = computed
  verify_user_password_hash?(computed)
end

#verify_user_password_v5?(password) ⇒ Boolean

Returns:

  • (Boolean)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 86

def verify_user_password_v5?(password)
  u_entry = @encrypt[:U] || "".b
  _hash, validation_salt, _key_salt = V5Writer.extract_v5_salts(u_entry)
  return false unless validation_salt

  candidate = V5Writer.hash_v5(password.to_s, validation_salt)
  stored_hash = u_entry.byteslice(0, 32)
  return false unless candidate[0, 32] == stored_hash

  # Password verified — derive and stash the file key so the
  # handler is immediately usable for decrypt/encrypt.
  @key = compute_encryption_key_v5(password)
  true
end