Module: Pdfrb::Encryption::PasswordVerification

Defined in:
lib/pdfrb/encryption/password_verification.rb

Overview

Password verification and key derivation for the Standard security handler (s7.6.3.3, Algorithms 2 / 3 / 5 / 6 / 7).

All functions are pure (no IO); they consume and return bytes.

Class Method Summary collapse

Class Method Details

.build_u_r2(file_key) ⇒ Object

Algorithm 4 (R=2): build the /U entry.



97
98
99
100
# File 'lib/pdfrb/encryption/password_verification.rb', line 97

def build_u_r2(file_key)
  rc4 = RC4.new(file_key)
  rc4.process(pack_bytes(PADDING_BYTES))
end

.build_u_r3plus(file_key:, id0:, revision:) ⇒ Object

Algorithm 5 (R>=3): build the /U entry.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/pdfrb/encryption/password_verification.rb', line 103

def build_u_r3plus(file_key:, id0:, revision:)
  digest = Digest::MD5.new
  digest.update(pack_bytes(PADDING_BYTES))
  digest.update(id0.b)
  hash = digest.digest
  rc4 = RC4.new(file_key)
  encrypted = rc4.process(hash)
  # 19 more rounds with key XORed by round index.
  19.times do |i|
    key = file_key.bytes.map { |b| (b ^ i).chr }.join
    encrypted = RC4.new(key).process(encrypted)
  end
  # Pad / truncate to 32 bytes.
  encrypted = encrypted + ("\x00" * (32 - encrypted.bytesize))
  encrypted.byteslice(0, 32)
end

.derive_key_rc4(password:, o_entry:, p_flags:, id0:, revision:, key_length_bits:, encrypt_metadata: true) ⇒ Object

Algorithm 2 (revision 2..4, RC4 and AES-128 V4): derive the encryption key from a user password.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/pdfrb/encryption/password_verification.rb', line 26

def derive_key_rc4(password:, o_entry:, p_flags:, id0:,
                   revision:, key_length_bits:, encrypt_metadata: true)
  padded = pad_password(password)
  digest = Digest::MD5.new
  digest.update(padded)
  digest.update(o_entry.b)
  digest.update([p_flags].pack("V"))
  digest.update(id0.b)
  unless 
    digest.update("\xFF\xFF\xFF\xFF".b)
  end
  hash = digest.digest
  if revision >= 3
    50.times { hash = Digest::MD5.digest(hash[0, key_length_bits / 8]) }
  end
  hash.byteslice(0, key_length_bits / 8)
end

.extract_v5_salts(entry) ⇒ Object

Extract the validation salt (bytes 32..40) and key salt (40..48) from a /U or /O entry for V5.



88
89
90
91
92
93
94
# File 'lib/pdfrb/encryption/password_verification.rb', line 88

def extract_v5_salts(entry)
  bytes = entry.b
  {
    validation_salt: bytes.byteslice(32, 8) || "".b,
    key_salt: bytes.byteslice(40, 8) || "".b,
  }
end

.pad_password(password) ⇒ Object



144
145
146
147
148
# File 'lib/pdfrb/encryption/password_verification.rb', line 144

def pad_password(password)
  bytes = password.to_s.b
  padded = bytes + pack_bytes(PADDING_BYTES)
  padded.byteslice(0, 32)
end

.verify_owner_password_v5(password:, o_entry:, u_entry:, validation_salt:) ⇒ Object

Algorithm 2.B / 9: same for owner password. Owner password validation uses /O + /U in the hash chain.



55
56
57
58
59
60
# File 'lib/pdfrb/encryption/password_verification.rb', line 55

def verify_owner_password_v5(password:, o_entry:, u_entry:,
                             validation_salt:)
  hash = sha256_password_rounds(password: password.b, salt: validation_salt.b,
                                u_entry: u_entry.b, o_entry: o_entry.b)
  hash.byteslice(0, 32) == o_entry.byteslice(0, 32) ? hash : nil
end

.verify_user_password(password:, encrypt_dict:, id0:) ⇒ Object

Algorithm 6: verify a user password. Returns true if it matches.



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/pdfrb/encryption/password_verification.rb', line 122

def verify_user_password(password:, encrypt_dict:, id0:)
  o_entry = encrypt_dict[:O].b
  p_flags = encrypt_dict[:P]
  revision = encrypt_dict[:R]
  key_bits = (encrypt_dict[:Length] || 40).to_i
   = encrypt_dict.fetch(:EncryptMetadata, true)
  key = derive_key_rc4(
    password: password, o_entry: o_entry, p_flags: p_flags,
    id0: id0, revision: revision, key_length_bits: key_bits,
    encrypt_metadata: 
  )
  expected_u = if revision == 2
                 build_u_r2(key)
               else
                 build_u_r3plus(file_key: key, id0: id0, revision: revision)
               end
  u_entry = encrypt_dict[:U].b
  # Compare the first 16 bytes for R>=3.
  slice_len = revision == 2 ? 32 : 16
  expected_u.byteslice(0, slice_len) == u_entry.byteslice(0, slice_len)
end

.verify_user_password_v5(password:, u_entry:, validation_salt:) ⇒ Object

Algorithm 2.B / 8 (revision >= 5, AES-256, PDF 2.0): verify a user password by hashing password + 8-byte validation salt from /U. Returns the 32-byte key on success, nil on mismatch.



47
48
49
50
51
# File 'lib/pdfrb/encryption/password_verification.rb', line 47

def verify_user_password_v5(password:, u_entry:, validation_salt:)
  hash = sha256_password_rounds(password: password.b, salt: validation_salt.b,
                                u_entry: u_entry.b)
  hash.byteslice(0, 32) == u_entry.byteslice(0, 32) ? hash : nil
end