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.



52
53
54
55
# File 'lib/pdfrb/encryption/password_verification.rb', line 52

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.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/pdfrb/encryption/password_verification.rb', line 58

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: derive the encryption key from a user password for revisions 2..4 (RC4 and AES-128).



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

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

.derive_key_v6(password:, u_entry:, o_entry:, p_flags:, oe_entry:) ⇒ Object

Algorithm 5: derive the encryption key for revision 6 (AES-256, PDF 2.0).

Raises:

  • (NotImplementedError)


45
46
47
48
49
# File 'lib/pdfrb/encryption/password_verification.rb', line 45

def derive_key_v6(password:, u_entry:, o_entry:,
                  p_flags:, oe_entry:)
  # PDF 2.0 uses SHA-256 with password + validation salt.
  raise NotImplementedError, "V6 key derivation TBD"
end

.pad_password(password) ⇒ Object



99
100
101
102
103
# File 'lib/pdfrb/encryption/password_verification.rb', line 99

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

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

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



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/pdfrb/encryption/password_verification.rb', line 77

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