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
-
.build_u_r2(file_key) ⇒ Object
Algorithm 4 (R=2): build the /U entry.
-
.build_u_r3plus(file_key:, id0:, revision:) ⇒ Object
Algorithm 5 (R>=3): build the /U entry.
-
.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).
-
.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).
- .pad_password(password) ⇒ Object
-
.verify_user_password(password:, encrypt_dict:, id0:) ⇒ Object
Algorithm 6: verify a user password.
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).
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 |