Class: Pdfrb::Encryption::PublicKeySecurityHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/pdfrb/encryption/public_key_security_handler.rb

Overview

Public-key security handler (PDF 1.7 §7.6.5, PKCS#7). Each recipient is encoded as a CMS EnvelopedData; the document encryption key is wrapped with the recipient's public key.

Read support: parses /Recipients array elements as PKCS#7 EnvelopedData, attempts decryption with a provided private key and certificate. Write support: builds EnvelopedData for a list of recipient certificates via OpenSSL::PKCS7.encrypt, returning DER bytes suitable for the /Recipients array.

Constant Summary collapse

DEFAULT_KEY_LENGTH =

AES-256

32

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(encrypt_dict: nil, recipients: [], private_key: nil, certificate: nil, file_key: nil) ⇒ PublicKeySecurityHandler

Returns a new instance of PublicKeySecurityHandler.

Parameters:

  • encrypt_dict (Hash, nil) (defaults to: nil)

    the /Encrypt dict (read-side).

  • recipients (Array<String>) (defaults to: [])

    /Recipients DER bytes.

  • private_key (OpenSSL::PKey::RSA, nil) (defaults to: nil)

    recipient's key.

  • certificate (OpenSSL::X509::Certificate, nil) (defaults to: nil)

    matching cert.

  • file_key (String, nil) (defaults to: nil)

    pre-derived file encryption key (write-side bypasses unwrap).



29
30
31
32
33
34
35
36
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 29

def initialize(encrypt_dict: nil, recipients: [], private_key: nil,
               certificate: nil, file_key: nil)
  @encrypt_dict = encrypt_dict
  @recipients = recipients
  @private_key = private_key
  @certificate = certificate
  @file_key = file_key
end

Instance Attribute Details

#certificateObject (readonly)

Returns the value of attribute certificate.



20
21
22
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 20

def certificate
  @certificate
end

#encrypt_dictObject (readonly)

Returns the value of attribute encrypt_dict.



20
21
22
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 20

def encrypt_dict
  @encrypt_dict
end

#file_keyObject (readonly)

Returns the value of attribute file_key.



20
21
22
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 20

def file_key
  @file_key
end

#private_keyObject (readonly)

Returns the value of attribute private_key.



20
21
22
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 20

def private_key
  @private_key
end

#recipientsObject (readonly)

Returns the value of attribute recipients.



20
21
22
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 20

def recipients
  @recipients
end

Class Method Details

.build_recipients(file_key:, recipient_certs:, cipher: "AES-256-CBC") ⇒ Object

Build /Recipients array of DER EnvelopedData bytes for each recipient cert. The file_key is wrapped into each envelope. Returns the Array of DER strings.



62
63
64
65
66
67
68
69
70
71
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 62

def self.build_recipients(file_key:, recipient_certs:, cipher: "AES-256-CBC")
  recipient_certs.map do |cert|
    pkcs7 = OpenSSL::PKCS7.encrypt(
      Array(cert), file_key,
      OpenSSL::Cipher.new(cipher),
      OpenSSL::PKCS7::BINARY
    )
    pkcs7.to_der
  end
end

.for_writer(recipient_certs:, cipher: "AES-256-CBC", key_length: DEFAULT_KEY_LENGTH) ⇒ Object

Construct a write-side handler. Generates a random DEFAULT_KEY_LENGTH-byte file_key, wraps it for each recipient cert, returns a handler whose encrypt_data is ready to use.



77
78
79
80
81
82
83
84
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 77

def self.for_writer(recipient_certs:, cipher: "AES-256-CBC",
                    key_length: DEFAULT_KEY_LENGTH)
  file_key = SecureRandom.random_bytes(key_length)
  recipients = build_recipients(file_key: file_key,
                                recipient_certs: recipient_certs,
                                cipher: cipher)
  new(recipients: recipients, file_key: file_key)
end

Instance Method Details

#can_decrypt?Boolean

Whether this handler can attempt decryption: needs a private key AND a matching certificate AND at least one recipient envelope.

Returns:

  • (Boolean)


41
42
43
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 41

def can_decrypt?
  !private_key.nil? && !certificate.nil? && recipients.any?
end

#decrypt_data(data, oid, gen) ⇒ Object



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

def decrypt_data(data, oid, gen)
  key = unwrap_key
  return data unless key
  return data if data.bytesize < 16

  obj_key = per_object_key(oid, gen, key)
  iv = data.byteslice(0, 16)
  ciphertext = data.byteslice(16..)
  decipher = OpenSSL::Cipher.new("AES-256-CBC")
  decipher.decrypt
  decipher.key = obj_key
  decipher.iv = iv
  decipher.update(ciphertext) + decipher.final
rescue OpenSSL::Cipher::CipherError
  data
end

#encrypt_data(data, oid, gen) ⇒ Object Also known as: encrypt

Per-object encryption: AES-256-CBC with IV prefix. The per-object key is SHA-256(file_key ‖ oid ‖ gen).



88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 88

def encrypt_data(data, oid, gen)
  return data unless @file_key

  obj_key = per_object_key(oid, gen)
  iv = SecureRandom.random_bytes(16)
  cipher = OpenSSL::Cipher.new("AES-256-CBC")
  cipher.encrypt
  cipher.key = obj_key
  cipher.iv = iv
  encrypted = cipher.update(data) + cipher.final
  iv + encrypted
end

#unwrap_keyObject

Attempt to unwrap the document encryption key using the private key. Returns the raw key bytes, or nil if the key couldn't be extracted.



48
49
50
51
52
53
54
55
56
57
# File 'lib/pdfrb/encryption/public_key_security_handler.rb', line 48

def unwrap_key
  return @file_key if @file_key
  return nil unless can_decrypt?

  recipients.each do |envelope_bytes|
    key = try_unwrap_envelope(envelope_bytes)
    return key if key
  end
  nil
end