Class: Pdfrb::Encryption::StandardSecurityHandler
- Inherits:
-
Object
- Object
- Pdfrb::Encryption::StandardSecurityHandler
- 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
-
#encrypt_dict ⇒ Object
readonly
Returns the value of attribute encrypt_dict.
-
#key ⇒ Object
readonly
Returns the value of attribute key.
-
#key_length ⇒ Object
readonly
Returns the value of attribute key_length.
-
#permissions ⇒ Object
readonly
Returns the value of attribute permissions.
-
#revision ⇒ Object
readonly
Returns the value of attribute revision.
-
#version ⇒ Object
readonly
Returns the value of attribute version.
Class Method Summary collapse
-
.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.
Instance Method Summary collapse
- #compute_encryption_key(password) ⇒ Object
-
#compute_encryption_key_v5(password) ⇒ Object
V5 key derivation: SHA-256 of (password + key_salt) gives an intermediate hash.
- #compute_object_key(oid, gen) ⇒ Object
-
#compute_object_key_v5(oid, gen) ⇒ Object
V5 per-object key: SHA-256 of (file_key || oid_le32 || gen_le32).
- #crypt_method_for(filter_name) ⇒ :aes256, ...
- #decrypt_data(data, oid, gen) ⇒ Object
- #decrypt_stream(data, oid, gen) ⇒ Object
- #decrypt_string(data, oid, gen) ⇒ Object
- #decrypt_with(method, data, oid, gen) ⇒ Object
- #encrypt_data(data, oid, gen) ⇒ Object (also: #encrypt)
- #encrypt_stream(data, oid, gen) ⇒ Object
-
#encrypt_string(data, oid, gen) ⇒ Object
Direction-aware entry points (s7.6.3.2 Table 21): streams use the StmF crypt filter, strings use StrF.
- #encrypt_with(method, data, oid, gen) ⇒ Object
-
#initialize(trailer_dict, precomputed_key: nil) ⇒ StandardSecurityHandler
constructor
A new instance of StandardSecurityHandler.
- #stm_crypt_method ⇒ Object
- #str_crypt_method ⇒ Object
- #verify_user_password?(password) ⇒ Boolean
- #verify_user_password_v5?(password) ⇒ Boolean
Constructor Details
#initialize(trailer_dict, precomputed_key: nil) ⇒ StandardSecurityHandler
Returns a new instance of StandardSecurityHandler.
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_dict ⇒ Object (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 |
#key ⇒ Object (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_length ⇒ Object (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 |
#permissions ⇒ Object (readonly)
Returns the value of attribute permissions.
13 14 15 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13 def @permissions end |
#revision ⇒ Object (readonly)
Returns the value of attribute revision.
13 14 15 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 13 def revision @revision end |
#version ⇒ Object (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 = "#{[ & 0xFFFFFFFF, 0xFFFFFFFF].pack('VV')}T#{SecureRandom.random_bytes(7)}" encrypt_dict = { Filter: :Standard, V: 5, R: 6, Length: key_length * 8, P: , 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 |
#crypt_method_for(filter_name) ⇒ :aes256, ...
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 210 def crypt_method_for(filter_name) return :aes256 if @version >= 5 return :rc4 if @version <= 3 # V4: resolve CFM from the CF dictionary; absent filters # default to Identity (s7.6.3.2 Table 21). return :identity if filter_name.nil? || filter_name == :Identity cf = @encrypt[:CF] cfm = cf && cf[filter_name] ? cf[filter_name][:CFM] : nil case cfm when :AESV2 then :aes when :V2 then :rc4 else :identity end end |
#decrypt_data(data, oid, gen) ⇒ Object
247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 247 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 |
#decrypt_stream(data, oid, gen) ⇒ Object
192 193 194 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 192 def decrypt_stream(data, oid, gen) decrypt_with(stm_crypt_method, data, oid, gen) end |
#decrypt_string(data, oid, gen) ⇒ Object
188 189 190 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 188 def decrypt_string(data, oid, gen) decrypt_with(str_crypt_method, data, oid, gen) end |
#decrypt_with(method, data, oid, gen) ⇒ Object
237 238 239 240 241 242 243 244 245 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 237 def decrypt_with(method, data, oid, gen) return data if method == :identity || @key.nil? obj_key = compute_object_key(oid, gen) case method when :aes256, :aes then 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 |
#encrypt_stream(data, oid, gen) ⇒ Object
184 185 186 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 184 def encrypt_stream(data, oid, gen) encrypt_with(stm_crypt_method, data, oid, gen) end |
#encrypt_string(data, oid, gen) ⇒ Object
Direction-aware entry points (s7.6.3.2 Table 21): streams use the StmF crypt filter, strings use StrF. V5 is always AES-256; V2/V3 are always RC4; V4 resolves the cipher from the CF dictionary's CFM (defaulting to Identity, per spec).
180 181 182 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 180 def encrypt_string(data, oid, gen) encrypt_with(str_crypt_method, data, oid, gen) end |
#encrypt_with(method, data, oid, gen) ⇒ Object
227 228 229 230 231 232 233 234 235 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 227 def encrypt_with(method, data, oid, gen) return data if method == :identity || @key.nil? obj_key = compute_object_key(oid, gen) case method when :aes256, :aes then encrypt_aes_cbc(data, obj_key) else encrypt_rc4(data, obj_key) end end |
#stm_crypt_method ⇒ Object
201 202 203 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 201 def stm_crypt_method crypt_method_for(@encrypt[:StmF]) end |
#str_crypt_method ⇒ Object
205 206 207 |
# File 'lib/pdfrb/encryption/standard_security_handler.rb', line 205 def str_crypt_method crypt_method_for(@encrypt[:StrF]) end |
#verify_user_password?(password) ⇒ 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
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 |