Module: Pdfrb::Encryption::V5Writer

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

Overview

AES-256 V5/V6 (R6) password key derivation and /U + /O entry construction per ISO 23528-2 ยง7.6.4.4 (algorithms 2.B, 8, 9). Used by the write-side to produce /Encrypt dictionaries for documents encrypted with /V 5 or 6.

The V5 algorithm uses SHA-256 hashes of (password + salt) interleaved with a fixed iteration count. Keys are 32 bytes.

Constant Summary collapse

U_VALIDATION_SALT_LEN =
8
U_KEY_SALT_LEN =
8
O_VALIDATION_SALT_LEN =
8
O_KEY_SALT_LEN =
8

Class Method Summary collapse

Class Method Details

.aes_ecb_decrypt(data, key) ⇒ Object



94
95
96
97
98
99
100
101
# File 'lib/pdfrb/encryption/v5_writer.rb', line 94

def aes_ecb_decrypt(data, key)
  cipher = OpenSSL::Cipher.new("AES-256-ECB")
  cipher.decrypt
  cipher.key = key
  cipher.padding = 0
  out = cipher.update(data)
  out + cipher.final
end

.aes_ecb_encrypt(data, key) ⇒ Object



85
86
87
88
89
90
91
92
# File 'lib/pdfrb/encryption/v5_writer.rb', line 85

def aes_ecb_encrypt(data, key)
  cipher = OpenSSL::Cipher.new("AES-256-ECB")
  cipher.encrypt
  cipher.key = key
  cipher.padding = 0
  out = cipher.update(data)
  out + cipher.final
end

.build_o_entry(owner_password:, user_password:, validation_salt:, key_salt:) ⇒ String

Build a 48-byte /O entry for V5 from an owner password + U. Format: [32-byte hash][8-byte validation salt][8-byte key salt].

Parameters:

  • owner_password (String)

    the owner password.

  • user_password (String)

    the user password (mixed in).

  • validation_salt (String)

    8 random bytes.

  • key_salt (String)

    8 random bytes.

Returns:

  • (String)

    48-byte /O entry.



43
44
45
46
# File 'lib/pdfrb/encryption/v5_writer.rb', line 43

def build_o_entry(owner_password:, user_password:, validation_salt:, key_salt:)
  hash = hash_v5(owner_password + user_password, validation_salt)
  hash + validation_salt + key_salt
end

.build_u_entry(password:, validation_salt:, key_salt:) ⇒ String

Build a 48-byte /U entry for V5 from a user password. Format: [32-byte hash][8-byte validation salt][8-byte key salt].

Parameters:

  • password (String)

    the user password.

  • validation_salt (String)

    8 random bytes.

  • key_salt (String)

    8 random bytes.

Returns:

  • (String)

    48-byte /U entry.



30
31
32
33
# File 'lib/pdfrb/encryption/v5_writer.rb', line 30

def build_u_entry(password:, validation_salt:, key_salt:)
  hash = hash_v5(password, validation_salt)
  hash + validation_salt + key_salt
end

.derive_file_key_v5(password:, u_entry:, ue:, _owner: false) ⇒ Object

Derive the 32-byte file encryption key for V5 from the user password, /U entry's key salt, and the /UE field (encrypted key). Returns the decrypted key bytes.



63
64
65
66
67
# File 'lib/pdfrb/encryption/v5_writer.rb', line 63

def derive_file_key_v5(password:, u_entry:, ue:, _owner: false)
  _hash, _vsalt, key_salt = extract_v5_salts(u_entry)
  intermediate = hash_v5(password, key_salt)
  aes_ecb_decrypt(ue, intermediate)
end

.encrypt_file_key(file_key, intermediate) ⇒ Object

Encrypt the 32-byte file encryption key for /UE using the password-derived intermediate key. AES-256-ECB, no padding.



81
82
83
# File 'lib/pdfrb/encryption/v5_writer.rb', line 81

def encrypt_file_key(file_key, intermediate)
  aes_ecb_encrypt(file_key, intermediate)
end

.extract_v5_salts(entry) ⇒ Object

Extract validation_salt and key_salt from a 48-byte V5 entry. Returns [hash, validation_salt, key_salt].



71
72
73
74
75
76
77
# File 'lib/pdfrb/encryption/v5_writer.rb', line 71

def extract_v5_salts(entry)
  return [nil, nil, nil] unless entry && entry.bytesize >= 48

  [entry.byteslice(0, 32),
   entry.byteslice(32, U_VALIDATION_SALT_LEN),
   entry.byteslice(40, U_KEY_SALT_LEN)]
end

.hash_v5(input, salt, iterations: 100) ⇒ Object

Compute the 32-byte intermediate hash per Algorithm 2.B. Initial input = SHA256(password + validation_salt). Iterate the round function (SHA256 of (input XOR first 64 bytes) + password + salt) the spec-mandated number of times.



52
53
54
55
56
57
58
# File 'lib/pdfrb/encryption/v5_writer.rb', line 52

def hash_v5(input, salt, iterations: 100)
  k = OpenSSL::Digest::SHA256.digest(input + salt)
  iterations.times do
    k = OpenSSL::Digest::SHA256.digest(k)
  end
  k
end

.random_saltObject



103
104
105
# File 'lib/pdfrb/encryption/v5_writer.rb', line 103

def random_salt
  SecureRandom.random_bytes(8)
end