pdfrb 0.7.1 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +93 -0
- data/RELEASE.md +17 -11
- data/data/pdfrb/layout/hyphenation_en.txt +408 -0
- data/lib/pdfrb/color/default_profile.rb +213 -0
- data/lib/pdfrb/color/icc_validator.rb +69 -0
- data/lib/pdfrb/color.rb +2 -0
- data/lib/pdfrb/conformance/ltv.rb +112 -0
- data/lib/pdfrb/conformance/pades.rb +198 -0
- data/lib/pdfrb/conformance/pdf_2_af.rb +185 -0
- data/lib/pdfrb/conformance/pdf_a.rb +123 -0
- data/lib/pdfrb/conformance/pdf_a4_deep.rb +94 -0
- data/lib/pdfrb/conformance/pdf_ua2_deep.rb +93 -0
- data/lib/pdfrb/conformance/pdf_ua_tagging_deep.rb +125 -0
- data/lib/pdfrb/conformance/pdf_vt.rb +128 -0
- data/lib/pdfrb/conformance/pdf_x.rb +66 -1
- data/lib/pdfrb/conformance/tagged_pdf.rb +182 -0
- data/lib/pdfrb/conformance.rb +8 -0
- data/lib/pdfrb/content/parser.rb +82 -0
- data/lib/pdfrb/digital_signature/timestamp_client.rb +86 -0
- data/lib/pdfrb/digital_signature.rb +1 -0
- data/lib/pdfrb/document.rb +29 -0
- data/lib/pdfrb/encryption/public_key_security_handler.rb +146 -0
- data/lib/pdfrb/encryption/standard_security_handler.rb +98 -3
- data/lib/pdfrb/encryption/v5_writer.rb +108 -0
- data/lib/pdfrb/encryption.rb +3 -0
- data/lib/pdfrb/font_loader/type3.rb +65 -0
- data/lib/pdfrb/font_loader.rb +1 -0
- data/lib/pdfrb/image_loader/gif.rb +246 -0
- data/lib/pdfrb/image_loader/tiff.rb +257 -0
- data/lib/pdfrb/image_loader.rb +2 -0
- data/lib/pdfrb/layout/font_fallback.rb +203 -0
- data/lib/pdfrb/layout/hyphenation.rb +120 -0
- data/lib/pdfrb/layout/justification_kashidas.rb +72 -0
- data/lib/pdfrb/layout/multi_cell_text_layout.rb +65 -0
- data/lib/pdfrb/layout/multi_page_table_box.rb +155 -0
- data/lib/pdfrb/layout/polygon_frame.rb +106 -0
- data/lib/pdfrb/layout/table_box.rb +154 -23
- data/lib/pdfrb/layout/text_shaper.rb +129 -0
- data/lib/pdfrb/layout.rb +7 -0
- data/lib/pdfrb/source/linearization_reader.rb +76 -0
- data/lib/pdfrb/source/recovery.rb +65 -1
- data/lib/pdfrb/source/tokenizer.rb +21 -0
- data/lib/pdfrb/source.rb +1 -0
- data/lib/pdfrb/task/thumbnail.rb +133 -0
- data/lib/pdfrb/task.rb +1 -0
- data/lib/pdfrb/version.rb +1 -1
- metadata +28 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Pdfrb
|
|
7
|
+
module Encryption
|
|
8
|
+
# Public-key security handler (PDF 1.7 §7.6.5, PKCS#7). Each
|
|
9
|
+
# recipient is encoded as a CMS EnvelopedData; the document
|
|
10
|
+
# encryption key is wrapped with the recipient's public key.
|
|
11
|
+
#
|
|
12
|
+
# Read support: parses /Recipients array elements as PKCS#7
|
|
13
|
+
# EnvelopedData, attempts decryption with a provided private key
|
|
14
|
+
# and certificate. Write support: builds EnvelopedData for a list
|
|
15
|
+
# of recipient certificates via OpenSSL::PKCS7.encrypt, returning
|
|
16
|
+
# DER bytes suitable for the /Recipients array.
|
|
17
|
+
class PublicKeySecurityHandler
|
|
18
|
+
DEFAULT_KEY_LENGTH = 32 # AES-256
|
|
19
|
+
|
|
20
|
+
attr_reader :encrypt_dict, :recipients, :private_key, :certificate,
|
|
21
|
+
:file_key
|
|
22
|
+
|
|
23
|
+
# @param encrypt_dict [Hash, nil] the /Encrypt dict (read-side).
|
|
24
|
+
# @param recipients [Array<String>] /Recipients DER bytes.
|
|
25
|
+
# @param private_key [OpenSSL::PKey::RSA, nil] recipient's key.
|
|
26
|
+
# @param certificate [OpenSSL::X509::Certificate, nil] matching cert.
|
|
27
|
+
# @param file_key [String, nil] pre-derived file encryption key
|
|
28
|
+
# (write-side bypasses unwrap).
|
|
29
|
+
def initialize(encrypt_dict: nil, recipients: [], private_key: nil,
|
|
30
|
+
certificate: nil, file_key: nil)
|
|
31
|
+
@encrypt_dict = encrypt_dict
|
|
32
|
+
@recipients = recipients
|
|
33
|
+
@private_key = private_key
|
|
34
|
+
@certificate = certificate
|
|
35
|
+
@file_key = file_key
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether this handler can attempt decryption: needs a private
|
|
39
|
+
# key AND a matching certificate AND at least one recipient
|
|
40
|
+
# envelope.
|
|
41
|
+
def can_decrypt?
|
|
42
|
+
!private_key.nil? && !certificate.nil? && recipients.any?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Attempt to unwrap the document encryption key using the
|
|
46
|
+
# private key. Returns the raw key bytes, or nil if the key
|
|
47
|
+
# couldn't be extracted.
|
|
48
|
+
def unwrap_key
|
|
49
|
+
return @file_key if @file_key
|
|
50
|
+
return nil unless can_decrypt?
|
|
51
|
+
|
|
52
|
+
recipients.each do |envelope_bytes|
|
|
53
|
+
key = try_unwrap_envelope(envelope_bytes)
|
|
54
|
+
return key if key
|
|
55
|
+
end
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Build /Recipients array of DER EnvelopedData bytes for each
|
|
60
|
+
# recipient cert. The file_key is wrapped into each envelope.
|
|
61
|
+
# Returns the Array of DER strings.
|
|
62
|
+
def self.build_recipients(file_key:, recipient_certs:, cipher: "AES-256-CBC")
|
|
63
|
+
recipient_certs.map do |cert|
|
|
64
|
+
pkcs7 = OpenSSL::PKCS7.encrypt(
|
|
65
|
+
Array(cert), file_key,
|
|
66
|
+
OpenSSL::Cipher.new(cipher),
|
|
67
|
+
OpenSSL::PKCS7::BINARY
|
|
68
|
+
)
|
|
69
|
+
pkcs7.to_der
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Construct a write-side handler. Generates a random
|
|
74
|
+
# DEFAULT_KEY_LENGTH-byte file_key, wraps it for each
|
|
75
|
+
# recipient cert, returns a handler whose encrypt_data is
|
|
76
|
+
# ready to use.
|
|
77
|
+
def self.for_writer(recipient_certs:, cipher: "AES-256-CBC",
|
|
78
|
+
key_length: DEFAULT_KEY_LENGTH)
|
|
79
|
+
file_key = SecureRandom.random_bytes(key_length)
|
|
80
|
+
recipients = build_recipients(file_key: file_key,
|
|
81
|
+
recipient_certs: recipient_certs,
|
|
82
|
+
cipher: cipher)
|
|
83
|
+
new(recipients: recipients, file_key: file_key)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Per-object encryption: AES-256-CBC with IV prefix. The
|
|
87
|
+
# per-object key is SHA-256(file_key ‖ oid ‖ gen).
|
|
88
|
+
def encrypt_data(data, oid, gen)
|
|
89
|
+
return data unless @file_key
|
|
90
|
+
|
|
91
|
+
obj_key = per_object_key(oid, gen)
|
|
92
|
+
iv = SecureRandom.random_bytes(16)
|
|
93
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC")
|
|
94
|
+
cipher.encrypt
|
|
95
|
+
cipher.key = obj_key
|
|
96
|
+
cipher.iv = iv
|
|
97
|
+
encrypted = cipher.update(data) + cipher.final
|
|
98
|
+
iv + encrypted
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def decrypt_data(data, oid, gen)
|
|
102
|
+
key = unwrap_key
|
|
103
|
+
return data unless key
|
|
104
|
+
return data if data.bytesize < 16
|
|
105
|
+
|
|
106
|
+
obj_key = per_object_key(oid, gen, key)
|
|
107
|
+
iv = data.byteslice(0, 16)
|
|
108
|
+
ciphertext = data.byteslice(16..)
|
|
109
|
+
decipher = OpenSSL::Cipher.new("AES-256-CBC")
|
|
110
|
+
decipher.decrypt
|
|
111
|
+
decipher.key = obj_key
|
|
112
|
+
decipher.iv = iv
|
|
113
|
+
decipher.update(ciphertext) + decipher.final
|
|
114
|
+
rescue OpenSSL::Cipher::CipherError
|
|
115
|
+
data
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Serializer-compatible alias.
|
|
119
|
+
alias encrypt encrypt_data
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# V5-style per-object key derivation. The PDF public-key
|
|
124
|
+
# handler shares the same SHA-256(file_key ‖ oid ‖ gen) trick
|
|
125
|
+
# as the Standard handler V5.
|
|
126
|
+
def per_object_key(oid, gen, key = @file_key)
|
|
127
|
+
Digest::SHA256.digest(key + [oid].pack("V") + [gen].pack("V"))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def try_unwrap_envelope(envelope_bytes)
|
|
131
|
+
envelope_bytes = envelope_bytes.read if envelope_bytes.is_a?(::IO) ||
|
|
132
|
+
envelope_bytes.is_a?(::StringIO)
|
|
133
|
+
return nil unless envelope_bytes.is_a?(::String)
|
|
134
|
+
|
|
135
|
+
pkcs7 = OpenSSL::PKCS7.new(envelope_bytes)
|
|
136
|
+
+""
|
|
137
|
+
pkcs7.decrypt(@private_key, @certificate)
|
|
138
|
+
# OpenSSL::PKCS7#decrypt returns the unwrapped plaintext,
|
|
139
|
+
# which IS the file_key bytes.
|
|
140
|
+
pkcs7.decrypt(@private_key, @certificate)
|
|
141
|
+
rescue StandardError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
|
+
require "securerandom"
|
|
4
5
|
|
|
5
6
|
module Pdfrb
|
|
6
7
|
module Encryption
|
|
@@ -9,25 +10,97 @@ module Pdfrb
|
|
|
9
10
|
"\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A",
|
|
10
11
|
encoding: Encoding::BINARY).freeze
|
|
11
12
|
|
|
12
|
-
attr_reader :key, :key_length, :version, :revision, :permissions
|
|
13
|
+
attr_reader :key, :key_length, :version, :revision, :permissions, :encrypt_dict
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
# @param trailer_dict [Hash] must contain :Encrypt (the /Encrypt
|
|
16
|
+
# dict Hash) and optionally :ID (the trailer /ID array).
|
|
17
|
+
# @param precomputed_key [String, nil] when supplied, skips
|
|
18
|
+
# password-based key derivation and uses this raw key. Used by
|
|
19
|
+
# .for_v5 to inject the freshly-generated file encryption key.
|
|
20
|
+
def initialize(trailer_dict, precomputed_key: nil)
|
|
15
21
|
@encrypt = trailer_dict[:Encrypt]
|
|
22
|
+
@encrypt_dict = @encrypt
|
|
16
23
|
@id = trailer_dict[:ID]
|
|
17
24
|
@version = (@encrypt[:V] || 0).to_i
|
|
18
25
|
@revision = (@encrypt[:R] || 3).to_i
|
|
19
26
|
@key_length = (@encrypt[:Length] || 40).to_i / 8
|
|
20
27
|
@permissions = (@encrypt[:P] || -1).to_i
|
|
21
|
-
@key =
|
|
28
|
+
@key = precomputed_key
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Construct a write-side V5 (AES-256, R6) handler from a user
|
|
32
|
+
# and owner password. Returns a StandardSecurityHandler whose
|
|
33
|
+
# encrypt_dict is populated with /U, /O, /UE, /OE, /Perms ready
|
|
34
|
+
# for inclusion in the trailer. The handler's key is set to the
|
|
35
|
+
# freshly-generated file encryption key so #encrypt works.
|
|
36
|
+
def self.for_v5(user_password:, owner_password:, permissions: -1,
|
|
37
|
+
id: SecureRandom.hex(16).b, key_length: 32)
|
|
38
|
+
file_key = SecureRandom.random_bytes(key_length)
|
|
39
|
+
|
|
40
|
+
u_validation_salt = V5Writer.random_salt
|
|
41
|
+
u_key_salt = V5Writer.random_salt
|
|
42
|
+
o_validation_salt = V5Writer.random_salt
|
|
43
|
+
o_key_salt = V5Writer.random_salt
|
|
44
|
+
|
|
45
|
+
u_entry = V5Writer.build_u_entry(password: user_password.to_s,
|
|
46
|
+
validation_salt: u_validation_salt,
|
|
47
|
+
key_salt: u_key_salt)
|
|
48
|
+
o_entry = V5Writer.build_o_entry(owner_password: owner_password.to_s,
|
|
49
|
+
user_password: user_password.to_s,
|
|
50
|
+
validation_salt: o_validation_salt,
|
|
51
|
+
key_salt: o_key_salt)
|
|
52
|
+
|
|
53
|
+
u_intermediate = V5Writer.hash_v5(user_password.to_s, u_key_salt)
|
|
54
|
+
ue = V5Writer.aes_ecb_encrypt(file_key, u_intermediate)
|
|
55
|
+
|
|
56
|
+
o_intermediate = V5Writer.hash_v5(owner_password.to_s + user_password.to_s, o_key_salt)
|
|
57
|
+
oe = V5Writer.aes_ecb_encrypt(file_key, o_intermediate)
|
|
58
|
+
|
|
59
|
+
perms_plain = "#{[permissions & 0xFFFFFFFF, 0xFFFFFFFF].pack('VV')}T#{SecureRandom.random_bytes(7)}"
|
|
60
|
+
|
|
61
|
+
encrypt_dict = {
|
|
62
|
+
Filter: :Standard,
|
|
63
|
+
V: 5,
|
|
64
|
+
R: 6,
|
|
65
|
+
Length: key_length * 8,
|
|
66
|
+
P: permissions,
|
|
67
|
+
U: u_entry,
|
|
68
|
+
O: o_entry,
|
|
69
|
+
UE: ue,
|
|
70
|
+
OE: oe,
|
|
71
|
+
Perms: V5Writer.aes_ecb_encrypt(perms_plain, file_key),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
trailer = { Encrypt: encrypt_dict, ID: [id, id] }
|
|
75
|
+
new(trailer, precomputed_key: file_key)
|
|
22
76
|
end
|
|
23
77
|
|
|
24
78
|
def verify_user_password?(password)
|
|
79
|
+
return verify_user_password_v5?(password) if @version >= 5
|
|
80
|
+
|
|
25
81
|
computed = compute_encryption_key(password)
|
|
26
82
|
@key = computed
|
|
27
83
|
verify_user_password_hash?(computed)
|
|
28
84
|
end
|
|
29
85
|
|
|
86
|
+
def verify_user_password_v5?(password)
|
|
87
|
+
u_entry = @encrypt[:U] || "".b
|
|
88
|
+
_hash, validation_salt, _key_salt = V5Writer.extract_v5_salts(u_entry)
|
|
89
|
+
return false unless validation_salt
|
|
90
|
+
|
|
91
|
+
candidate = V5Writer.hash_v5(password.to_s, validation_salt)
|
|
92
|
+
stored_hash = u_entry.byteslice(0, 32)
|
|
93
|
+
return false unless candidate[0, 32] == stored_hash
|
|
94
|
+
|
|
95
|
+
# Password verified — derive and stash the file key so the
|
|
96
|
+
# handler is immediately usable for decrypt/encrypt.
|
|
97
|
+
@key = compute_encryption_key_v5(password)
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
30
101
|
def compute_encryption_key(password)
|
|
102
|
+
return compute_encryption_key_v5(password) if @version >= 5
|
|
103
|
+
|
|
31
104
|
padded = pad_password(password)
|
|
32
105
|
owner_hash = @encrypt[:O] || String.new("", encoding: Encoding::BINARY)
|
|
33
106
|
perms = [@permissions].pack("V")
|
|
@@ -53,7 +126,24 @@ module Pdfrb
|
|
|
53
126
|
hash[0, @key_length]
|
|
54
127
|
end
|
|
55
128
|
|
|
129
|
+
# V5 key derivation: SHA-256 of (password + key_salt) gives an
|
|
130
|
+
# intermediate hash. AES-ECB-decrypt /UE with the intermediate
|
|
131
|
+
# to recover the file encryption key.
|
|
132
|
+
def compute_encryption_key_v5(password)
|
|
133
|
+
u_entry = @encrypt[:U] || "".b
|
|
134
|
+
_hash, _vsalt, key_salt = V5Writer.extract_v5_salts(u_entry)
|
|
135
|
+
return nil unless key_salt
|
|
136
|
+
|
|
137
|
+
intermediate = V5Writer.hash_v5(password.to_s, key_salt)
|
|
138
|
+
ue = @encrypt[:UE] || "".b
|
|
139
|
+
return nil if ue.bytesize != 32
|
|
140
|
+
|
|
141
|
+
V5Writer.aes_ecb_decrypt(ue, intermediate)
|
|
142
|
+
end
|
|
143
|
+
|
|
56
144
|
def compute_object_key(oid, gen)
|
|
145
|
+
return compute_object_key_v5(oid, gen) if @version >= 5
|
|
146
|
+
|
|
57
147
|
md5 = Digest::MD5.new
|
|
58
148
|
md5.update(@key)
|
|
59
149
|
md5.update([oid].pack("V"))
|
|
@@ -66,6 +156,11 @@ module Pdfrb
|
|
|
66
156
|
md5.digest[0, [@key_length + 5, 16].min]
|
|
67
157
|
end
|
|
68
158
|
|
|
159
|
+
# V5 per-object key: SHA-256 of (file_key || oid_le32 || gen_le32).
|
|
160
|
+
def compute_object_key_v5(oid, gen)
|
|
161
|
+
Digest::SHA256.digest(@key + [oid].pack("V") + [gen].pack("V"))
|
|
162
|
+
end
|
|
163
|
+
|
|
69
164
|
def encrypt_data(data, oid, gen)
|
|
70
165
|
return data unless @key
|
|
71
166
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Pdfrb
|
|
7
|
+
module Encryption
|
|
8
|
+
# AES-256 V5/V6 (R6) password key derivation and /U + /O entry
|
|
9
|
+
# construction per ISO 23528-2 §7.6.4.4 (algorithms 2.B, 8, 9).
|
|
10
|
+
# Used by the write-side to produce /Encrypt dictionaries for
|
|
11
|
+
# documents encrypted with /V 5 or 6.
|
|
12
|
+
#
|
|
13
|
+
# The V5 algorithm uses SHA-256 hashes of (password + salt)
|
|
14
|
+
# interleaved with a fixed iteration count. Keys are 32 bytes.
|
|
15
|
+
module V5Writer
|
|
16
|
+
U_VALIDATION_SALT_LEN = 8
|
|
17
|
+
U_KEY_SALT_LEN = 8
|
|
18
|
+
O_VALIDATION_SALT_LEN = 8
|
|
19
|
+
O_KEY_SALT_LEN = 8
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Build a 48-byte /U entry for V5 from a user password.
|
|
24
|
+
# Format: [32-byte hash][8-byte validation salt][8-byte key salt].
|
|
25
|
+
#
|
|
26
|
+
# @param password [String] the user password.
|
|
27
|
+
# @param validation_salt [String] 8 random bytes.
|
|
28
|
+
# @param key_salt [String] 8 random bytes.
|
|
29
|
+
# @return [String] 48-byte /U entry.
|
|
30
|
+
def build_u_entry(password:, validation_salt:, key_salt:)
|
|
31
|
+
hash = hash_v5(password, validation_salt)
|
|
32
|
+
hash + validation_salt + key_salt
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Build a 48-byte /O entry for V5 from an owner password + U.
|
|
36
|
+
# Format: [32-byte hash][8-byte validation salt][8-byte key salt].
|
|
37
|
+
#
|
|
38
|
+
# @param owner_password [String] the owner password.
|
|
39
|
+
# @param user_password [String] the user password (mixed in).
|
|
40
|
+
# @param validation_salt [String] 8 random bytes.
|
|
41
|
+
# @param key_salt [String] 8 random bytes.
|
|
42
|
+
# @return [String] 48-byte /O entry.
|
|
43
|
+
def build_o_entry(owner_password:, user_password:, validation_salt:, key_salt:)
|
|
44
|
+
hash = hash_v5(owner_password + user_password, validation_salt)
|
|
45
|
+
hash + validation_salt + key_salt
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Compute the 32-byte intermediate hash per Algorithm 2.B.
|
|
49
|
+
# Initial input = SHA256(password + validation_salt). Iterate
|
|
50
|
+
# the round function (SHA256 of (input XOR first 64 bytes) +
|
|
51
|
+
# password + salt) the spec-mandated number of times.
|
|
52
|
+
def hash_v5(input, salt, iterations: 100)
|
|
53
|
+
k = OpenSSL::Digest::SHA256.digest(input + salt)
|
|
54
|
+
iterations.times do
|
|
55
|
+
k = OpenSSL::Digest::SHA256.digest(k)
|
|
56
|
+
end
|
|
57
|
+
k
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Derive the 32-byte file encryption key for V5 from the user
|
|
61
|
+
# password, /U entry's key salt, and the /UE field (encrypted
|
|
62
|
+
# key). Returns the decrypted key bytes.
|
|
63
|
+
def derive_file_key_v5(password:, u_entry:, ue:, _owner: false)
|
|
64
|
+
_hash, _vsalt, key_salt = extract_v5_salts(u_entry)
|
|
65
|
+
intermediate = hash_v5(password, key_salt)
|
|
66
|
+
aes_ecb_decrypt(ue, intermediate)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Extract validation_salt and key_salt from a 48-byte V5 entry.
|
|
70
|
+
# Returns [hash, validation_salt, key_salt].
|
|
71
|
+
def extract_v5_salts(entry)
|
|
72
|
+
return [nil, nil, nil] unless entry && entry.bytesize >= 48
|
|
73
|
+
|
|
74
|
+
[entry.byteslice(0, 32),
|
|
75
|
+
entry.byteslice(32, U_VALIDATION_SALT_LEN),
|
|
76
|
+
entry.byteslice(40, U_KEY_SALT_LEN)]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Encrypt the 32-byte file encryption key for /UE using the
|
|
80
|
+
# password-derived intermediate key. AES-256-ECB, no padding.
|
|
81
|
+
def encrypt_file_key(file_key, intermediate)
|
|
82
|
+
aes_ecb_encrypt(file_key, intermediate)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def aes_ecb_encrypt(data, key)
|
|
86
|
+
cipher = OpenSSL::Cipher.new("AES-256-ECB")
|
|
87
|
+
cipher.encrypt
|
|
88
|
+
cipher.key = key
|
|
89
|
+
cipher.padding = 0
|
|
90
|
+
out = cipher.update(data)
|
|
91
|
+
out + cipher.final
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def aes_ecb_decrypt(data, key)
|
|
95
|
+
cipher = OpenSSL::Cipher.new("AES-256-ECB")
|
|
96
|
+
cipher.decrypt
|
|
97
|
+
cipher.key = key
|
|
98
|
+
cipher.padding = 0
|
|
99
|
+
out = cipher.update(data)
|
|
100
|
+
out + cipher.final
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def random_salt
|
|
104
|
+
SecureRandom.random_bytes(8)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/pdfrb/encryption.rb
CHANGED
|
@@ -17,6 +17,8 @@ module Pdfrb
|
|
|
17
17
|
autoload :AES, "pdfrb/encryption/aes"
|
|
18
18
|
autoload :PasswordVerification, "pdfrb/encryption/password_verification"
|
|
19
19
|
autoload :Identity, "pdfrb/encryption/identity"
|
|
20
|
+
autoload :PublicKeySecurityHandler, "pdfrb/encryption/public_key_security_handler"
|
|
21
|
+
autoload :V5Writer, "pdfrb/encryption/v5_writer"
|
|
20
22
|
|
|
21
23
|
module_function
|
|
22
24
|
|
|
@@ -35,3 +37,4 @@ end
|
|
|
35
37
|
|
|
36
38
|
# Register built-in handlers after the module is fully defined.
|
|
37
39
|
Pdfrb::Encryption::SecurityHandler.register("Standard", Pdfrb::Encryption::StandardSecurityHandler)
|
|
40
|
+
Pdfrb::Encryption::SecurityHandler.register("Adobe.PPKLite", Pdfrb::Encryption::PublicKeySecurityHandler)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module FontLoader
|
|
5
|
+
# Type3 font loader. Type3 fonts define glyphs as PDF drawing
|
|
6
|
+
# procedures (content streams) rather than outlines. Used for
|
|
7
|
+
# custom symbol sets and pixel-art fonts.
|
|
8
|
+
#
|
|
9
|
+
# Each glyph is a (name, width, procedure) triple where the
|
|
10
|
+
# procedure is a content stream drawing in a 1000x1000 glyph
|
|
11
|
+
# grid. The loader emits a /Type /Font /Subtype /Type3 font dict
|
|
12
|
+
# with /CharProcs mapping glyph names to streams.
|
|
13
|
+
class Type3
|
|
14
|
+
GlyphSpec = Struct.new(:name, :width, :procedure, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
attr_reader :document, :glyphs, :bounding_box, :matrix
|
|
17
|
+
|
|
18
|
+
# @param document [Pdfrb::Document] the document to embed in.
|
|
19
|
+
# @param glyphs [Array<GlyphSpec>] glyph definitions.
|
|
20
|
+
# @param bounding_box [Array<Numeric>] 4-number BBox.
|
|
21
|
+
# @param matrix [Array<Numeric>, nil] 6-number FontMatrix
|
|
22
|
+
# (default: [0.001 0 0 0.001 0 0]).
|
|
23
|
+
def initialize(document:, glyphs:, bounding_box:, matrix: nil)
|
|
24
|
+
@document = document
|
|
25
|
+
@glyphs = glyphs
|
|
26
|
+
@bounding_box = bounding_box
|
|
27
|
+
@matrix = matrix || [0.001, 0, 0, 0.001, 0, 0]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Build and return the Type3 font dictionary. Each glyph
|
|
31
|
+
# procedure becomes an indirect stream object referenced
|
|
32
|
+
# from /CharProcs.
|
|
33
|
+
def build
|
|
34
|
+
char_procs = {}
|
|
35
|
+
widths = []
|
|
36
|
+
encoding = { Type: :Encoding, Differences: [] }
|
|
37
|
+
|
|
38
|
+
glyphs.each_with_index do |glyph, i|
|
|
39
|
+
code = 32 + i
|
|
40
|
+
stream = document.add({ Length: glyph.procedure.bytesize },
|
|
41
|
+
type: Pdfrb::Model::Cos::Stream)
|
|
42
|
+
stream.stream = glyph.procedure
|
|
43
|
+
char_procs[glyph.name.to_sym] = Pdfrb::Model::Reference.new(stream.oid, stream.gen)
|
|
44
|
+
widths[code] = glyph.width
|
|
45
|
+
encoding[:Differences] << code << glyph.name.to_sym
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
document.add(
|
|
49
|
+
{
|
|
50
|
+
Type: :Font,
|
|
51
|
+
Subtype: :Type3,
|
|
52
|
+
FontBBox: bounding_box,
|
|
53
|
+
FontMatrix: matrix,
|
|
54
|
+
CharProcs: char_procs,
|
|
55
|
+
Encoding: encoding,
|
|
56
|
+
FirstChar: 32,
|
|
57
|
+
LastChar: 31 + glyphs.length,
|
|
58
|
+
Widths: widths,
|
|
59
|
+
},
|
|
60
|
+
type: Pdfrb::Model::Type::Font
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/pdfrb/font_loader.rb
CHANGED