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 (revision 2..4, RC4 and AES-128 V4): derive the encryption key from a user password.
-
.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.
- .pad_password(password) ⇒ Object
-
.verify_owner_password_v5(password:, o_entry:, u_entry:, validation_salt:) ⇒ Object
Algorithm 2.B / 9: same for owner password.
-
.verify_user_password(password:, encrypt_dict:, id0:) ⇒ Object
Algorithm 6: verify a user password.
-
.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.
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
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 |