xlsxrb 0.1.8 → 0.1.9
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 +15 -2
- data/README.md +50 -13
- data/Rakefile +20 -2
- data/docs/DEVELOPMENT.md +11 -3
- data/docs/QUALITY_ASSURANCE.md +3 -4
- data/docs/SPEC_SOURCES.md +7 -1
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/xlsxrb/elements/row.rb +2 -2
- data/lib/xlsxrb/elements/workbook.rb +1 -1
- data/lib/xlsxrb/elements/worksheet.rb +4 -4
- data/lib/xlsxrb/ooxml/cfb.rb +490 -0
- data/lib/xlsxrb/ooxml/crypto/agile.rb +285 -0
- data/lib/xlsxrb/ooxml/crypto/standard.rb +152 -0
- data/lib/xlsxrb/ooxml/crypto.rb +60 -0
- data/lib/xlsxrb/stream_row.rb +2 -2
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb.rb +129 -38
- data/sig/generated/xlsxrb/elements/worksheet.rbs +8 -5
- data/sig/generated/xlsxrb/ooxml/cfb.rbs +121 -0
- data/sig/generated/xlsxrb/ooxml/crypto/agile.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/crypto/standard.rbs +28 -0
- data/sig/generated/xlsxrb/ooxml/crypto.rbs +17 -0
- data/sig/generated/xlsxrb/stream_row.rbs +4 -2
- data/sig/generated/xlsxrb.rbs +40 -18
- data/vendor/sdk_runner/Program.cs +3 -1
- metadata +9 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "rexml/document"
|
|
7
|
+
require "securerandom"
|
|
8
|
+
|
|
9
|
+
module Xlsxrb
|
|
10
|
+
module Ooxml
|
|
11
|
+
module Crypto
|
|
12
|
+
# Implements Microsoft Office Agile Encryption specified in [MS-OFFCRYPTO] Section 2.3.4.
|
|
13
|
+
class Agile
|
|
14
|
+
BLOCK_KEY_VERIFIER_INPUT = "\xfe\xa7\xd2\x76\x3b\x4b\x9e\x79".b.freeze
|
|
15
|
+
BLOCK_KEY_VERIFIER_VALUE = "\xd7\xaa\x0f\x6d\x30\x61\x34\x4e".b.freeze
|
|
16
|
+
BLOCK_KEY_KEY = "\x14\x6e\x0b\xe7\xab\xac\xd0\xd6".b.freeze
|
|
17
|
+
BLOCK_KEY_INTEGRITY_KEY = "\x5f\xb2\xad\x01\x0c\xb9\xe1\xf6".b.freeze
|
|
18
|
+
BLOCK_KEY_INTEGRITY_VALUE = "\xa0\x67\x7f\x02\xb2\x2c\x84\x33".b.freeze
|
|
19
|
+
|
|
20
|
+
SEGMENT_SIZE = 4096
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Decrypts an encrypted package given the EncryptionInfo stream data and EncryptedPackage stream data.
|
|
24
|
+
def decrypt(encryption_info_bytes, encrypted_package_bytes, password)
|
|
25
|
+
info = parse_encryption_info(encryption_info_bytes)
|
|
26
|
+
password_str = password.to_s
|
|
27
|
+
|
|
28
|
+
digest = digest_for(info[:hash_algorithm])
|
|
29
|
+
cipher_name = cipher_name_for(info[:cipher_algorithm], info[:key_bits])
|
|
30
|
+
|
|
31
|
+
# 1. Derive intermediate hash H_final from password and keyEncryptor salt
|
|
32
|
+
h_final = derive_h_final(digest, password_str, info[:encryptor_salt], info[:spin_count])
|
|
33
|
+
|
|
34
|
+
# 2. Verify password with verifier input and hash
|
|
35
|
+
key_bytes = info[:key_bits] / 8
|
|
36
|
+
k_ver_input = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_INPUT, key_bytes)
|
|
37
|
+
verifier_input = aes_decrypt(cipher_name, k_ver_input, info[:encryptor_salt], info[:encrypted_verifier_hash_input])
|
|
38
|
+
|
|
39
|
+
expected_hash = digest.digest(verifier_input)
|
|
40
|
+
|
|
41
|
+
k_ver_val = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_VALUE, key_bytes)
|
|
42
|
+
decrypted_hash = aes_decrypt(cipher_name, k_ver_val, info[:encryptor_salt], info[:encrypted_verifier_hash_value])
|
|
43
|
+
|
|
44
|
+
hash_size = info[:hash_size]
|
|
45
|
+
raise Xlsxrb::InvalidPasswordError, "Password verification failed" unless OpenSSL.secure_compare(decrypted_hash[0, hash_size], expected_hash[0, hash_size])
|
|
46
|
+
|
|
47
|
+
# 3. Decrypt package master key
|
|
48
|
+
k_key = derive_key(digest, h_final, BLOCK_KEY_KEY, key_bytes)
|
|
49
|
+
package_key = aes_decrypt(cipher_name, k_key, info[:encryptor_salt], info[:encrypted_key_value])[0, key_bytes]
|
|
50
|
+
|
|
51
|
+
# 4. Decrypt package stream
|
|
52
|
+
raise Xlsxrb::DecryptionError, "Encrypted package stream is too short" if encrypted_package_bytes.bytesize < 8
|
|
53
|
+
|
|
54
|
+
total_size = encrypted_package_bytes[0, 8].unpack1("Q<")
|
|
55
|
+
raise Xlsxrb::DecryptionError, "Encrypted package size is invalid or exceeds limits" if total_size.negative? || total_size > 0x400_000_000
|
|
56
|
+
|
|
57
|
+
encrypted_data = encrypted_package_bytes[8..] || "".b
|
|
58
|
+
|
|
59
|
+
pkg_cipher_name = cipher_name_for(info[:key_data_cipher_algorithm], info[:key_data_key_bits])
|
|
60
|
+
pkg_digest = digest_for(info[:key_data_hash_algorithm])
|
|
61
|
+
pkg_salt = info[:key_data_salt]
|
|
62
|
+
|
|
63
|
+
decrypted = +""
|
|
64
|
+
num_blocks = (encrypted_data.bytesize + SEGMENT_SIZE - 1) / SEGMENT_SIZE
|
|
65
|
+
num_blocks.times do |i|
|
|
66
|
+
chunk = encrypted_data[i * SEGMENT_SIZE, SEGMENT_SIZE]
|
|
67
|
+
break if chunk.nil? || chunk.empty?
|
|
68
|
+
|
|
69
|
+
iv = pkg_digest.digest(pkg_salt + [i].pack("V"))[0, 16]
|
|
70
|
+
decrypted << aes_decrypt_block(pkg_cipher_name, package_key, iv, chunk)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
decrypted[0, total_size] || "".b
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Encrypts a plain zip payload with the given password into EncryptionInfo and EncryptedPackage streams.
|
|
77
|
+
def encrypt(plain_bytes, password)
|
|
78
|
+
password_str = password.to_s
|
|
79
|
+
spin_count = 100_000
|
|
80
|
+
key_bits = 256
|
|
81
|
+
key_bytes = key_bits / 8
|
|
82
|
+
hash_algorithm = "SHA512"
|
|
83
|
+
cipher_algorithm = "AES"
|
|
84
|
+
cipher_chaining = "ChainingModeCBC"
|
|
85
|
+
hash_size = 64
|
|
86
|
+
block_size = 16
|
|
87
|
+
|
|
88
|
+
digest = digest_for(hash_algorithm)
|
|
89
|
+
cipher_name = "aes-256-cbc"
|
|
90
|
+
|
|
91
|
+
encryptor_salt = SecureRandom.random_bytes(16)
|
|
92
|
+
key_data_salt = SecureRandom.random_bytes(16)
|
|
93
|
+
package_key = SecureRandom.random_bytes(key_bytes)
|
|
94
|
+
verifier_input = SecureRandom.random_bytes(16)
|
|
95
|
+
|
|
96
|
+
# Derive intermediate hash H_final
|
|
97
|
+
h_final = derive_h_final(digest, password_str, encryptor_salt, spin_count)
|
|
98
|
+
|
|
99
|
+
# Encrypt verifier input
|
|
100
|
+
k_ver_input = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_INPUT, key_bytes)
|
|
101
|
+
encrypted_verifier_input = aes_encrypt(cipher_name, k_ver_input, encryptor_salt, verifier_input)
|
|
102
|
+
|
|
103
|
+
# Encrypt verifier hash value
|
|
104
|
+
expected_hash = digest.digest(verifier_input)
|
|
105
|
+
k_ver_val = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_VALUE, key_bytes)
|
|
106
|
+
encrypted_verifier_val = aes_encrypt(cipher_name, k_ver_val, encryptor_salt, expected_hash)
|
|
107
|
+
|
|
108
|
+
# Encrypt package master key
|
|
109
|
+
k_key = derive_key(digest, h_final, BLOCK_KEY_KEY, key_bytes)
|
|
110
|
+
encrypted_key_value = aes_encrypt(cipher_name, k_key, encryptor_salt, package_key)
|
|
111
|
+
|
|
112
|
+
# Encrypt plain package data in 4096-byte segments
|
|
113
|
+
encrypted_package_stream = +""
|
|
114
|
+
encrypted_package_stream << [plain_bytes.bytesize].pack("Q<")
|
|
115
|
+
|
|
116
|
+
num_blocks = (plain_bytes.bytesize + SEGMENT_SIZE - 1) / SEGMENT_SIZE
|
|
117
|
+
num_blocks.times do |i|
|
|
118
|
+
chunk = plain_bytes[i * SEGMENT_SIZE, SEGMENT_SIZE] || "".b
|
|
119
|
+
# Pad last chunk to multiple of block_size if necessary
|
|
120
|
+
if (chunk.bytesize % block_size) != 0
|
|
121
|
+
pad_len = block_size - (chunk.bytesize % block_size)
|
|
122
|
+
chunk += ("\x00".b * pad_len)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
iv = digest.digest(key_data_salt + [i].pack("V"))[0, 16]
|
|
126
|
+
encrypted_package_stream << aes_encrypt_block(cipher_name, package_key, iv, chunk)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Data integrity HMAC
|
|
130
|
+
hmac_key = SecureRandom.random_bytes(64)
|
|
131
|
+
hmac_value = OpenSSL::HMAC.digest(hash_algorithm, hmac_key, plain_bytes)
|
|
132
|
+
|
|
133
|
+
k_integ_key = derive_key(digest, h_final, BLOCK_KEY_INTEGRITY_KEY, key_bytes)
|
|
134
|
+
encrypted_hmac_key = aes_encrypt(cipher_name, k_integ_key, encryptor_salt, hmac_key)
|
|
135
|
+
|
|
136
|
+
k_integ_val = derive_key(digest, h_final, BLOCK_KEY_INTEGRITY_VALUE, key_bytes)
|
|
137
|
+
encrypted_hmac_val = aes_encrypt(cipher_name, k_integ_val, encryptor_salt, hmac_value)
|
|
138
|
+
|
|
139
|
+
# Build EncryptionInfo XML
|
|
140
|
+
xml = <<~XML
|
|
141
|
+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
142
|
+
<encryption xmlns="http://schemas.microsoft.com/office/2006/encryption" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate">
|
|
143
|
+
<keyData saltSize="#{key_data_salt.bytesize}" blockSize="#{block_size}" keyBits="#{key_bits}" hashSize="#{hash_size}" cipherAlgorithm="#{cipher_algorithm}" cipherChaining="#{cipher_chaining}" hashAlgorithm="#{hash_algorithm}" saltValue="#{[key_data_salt].pack("m0")}"/>
|
|
144
|
+
<dataIntegrity encryptedHmacKey="#{[encrypted_hmac_key].pack("m0")}" encryptedHmacValue="#{[encrypted_hmac_val].pack("m0")}"/>
|
|
145
|
+
<keyEncryptors>
|
|
146
|
+
<keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
|
|
147
|
+
<p:encryptedKey spinCount="#{spin_count}" saltSize="#{encryptor_salt.bytesize}" blockSize="#{block_size}" keyBits="#{key_bits}" hashSize="#{hash_size}" cipherAlgorithm="#{cipher_algorithm}" cipherChaining="#{cipher_chaining}" hashAlgorithm="#{hash_algorithm}" saltValue="#{[encryptor_salt].pack("m0")}" encryptedVerifierHashInput="#{[encrypted_verifier_input].pack("m0")}" encryptedVerifierHashValue="#{[encrypted_verifier_val].pack("m0")}" encryptedKeyValue="#{[encrypted_key_value].pack("m0")}"/>
|
|
148
|
+
</keyEncryptor>
|
|
149
|
+
</keyEncryptors>
|
|
150
|
+
</encryption>
|
|
151
|
+
XML
|
|
152
|
+
|
|
153
|
+
# EncryptionInfo stream header: versionMajor=4, versionMinor=4, flags=0x40
|
|
154
|
+
info_stream = [4, 4, 0x40].pack("vvV") + xml.b
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
"EncryptionInfo" => info_stream,
|
|
158
|
+
"EncryptedPackage" => encrypted_package_stream
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def parse_encryption_info(bytes)
|
|
165
|
+
raise Xlsxrb::DecryptionError, "EncryptionInfo stream is too short" if bytes.bytesize < 8
|
|
166
|
+
|
|
167
|
+
major, minor, = bytes[0, 8].unpack("vvV")
|
|
168
|
+
raise Xlsxrb::DecryptionError, "Unsupported encryption version #{major}.#{minor}" unless major == 4 && minor == 4
|
|
169
|
+
|
|
170
|
+
xml_str = bytes[8..] || ""
|
|
171
|
+
begin
|
|
172
|
+
doc = REXML::Document.new(xml_str)
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
raise Xlsxrb::DecryptionError, "Malformed EncryptionInfo XML: #{e.message}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
root = doc.root
|
|
178
|
+
raise Xlsxrb::DecryptionError, "Invalid EncryptionInfo XML" unless root
|
|
179
|
+
|
|
180
|
+
key_data = root.elements["keyData"]
|
|
181
|
+
key_encryptor = root.elements["keyEncryptors/keyEncryptor/encryptedKey"] || root.elements["keyEncryptors/keyEncryptor/*"]
|
|
182
|
+
raise Xlsxrb::DecryptionError, "Missing keyEncryptor element" unless key_encryptor
|
|
183
|
+
|
|
184
|
+
spin_count = key_encryptor.attributes["spinCount"]&.to_i || 100_000
|
|
185
|
+
raise Xlsxrb::DecryptionError, "spinCount out of allowed bounds: #{spin_count}" if spin_count.negative? || spin_count > 10_000_000
|
|
186
|
+
|
|
187
|
+
key_attrs = key_data&.attributes
|
|
188
|
+
{
|
|
189
|
+
key_data_salt: key_attrs&.[]("saltValue").to_s.unpack1("m0") || "".b,
|
|
190
|
+
key_data_hash_algorithm: key_attrs&.[]("hashAlgorithm") || "SHA512",
|
|
191
|
+
key_data_cipher_algorithm: key_attrs&.[]("cipherAlgorithm") || "AES",
|
|
192
|
+
key_data_key_bits: key_attrs&.[]("keyBits")&.to_i || 256,
|
|
193
|
+
key_data_block_size: key_attrs&.[]("blockSize")&.to_i || 16,
|
|
194
|
+
|
|
195
|
+
spin_count: spin_count,
|
|
196
|
+
encryptor_salt: key_encryptor.attributes["saltValue"].to_s.unpack1("m0") || "".b,
|
|
197
|
+
hash_algorithm: key_encryptor.attributes["hashAlgorithm"] || "SHA512",
|
|
198
|
+
cipher_algorithm: key_encryptor.attributes["cipherAlgorithm"] || "AES",
|
|
199
|
+
key_bits: key_encryptor.attributes["keyBits"]&.to_i || 256,
|
|
200
|
+
hash_size: key_encryptor.attributes["hashSize"]&.to_i || 64,
|
|
201
|
+
encrypted_verifier_hash_input: key_encryptor.attributes["encryptedVerifierHashInput"].to_s.unpack1("m0") || "".b,
|
|
202
|
+
encrypted_verifier_hash_value: key_encryptor.attributes["encryptedVerifierHashValue"].to_s.unpack1("m0") || "".b,
|
|
203
|
+
encrypted_key_value: key_encryptor.attributes["encryptedKeyValue"].to_s.unpack1("m0") || "".b
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def digest_for(name)
|
|
208
|
+
case name.to_s.upcase.gsub(/[^A-Z0-9]/, "")
|
|
209
|
+
when "SHA512" then OpenSSL::Digest.new("SHA512")
|
|
210
|
+
when "SHA384" then OpenSSL::Digest.new("SHA384")
|
|
211
|
+
when "SHA256" then OpenSSL::Digest.new("SHA256")
|
|
212
|
+
when "SHA1" then OpenSSL::Digest.new("SHA1")
|
|
213
|
+
else
|
|
214
|
+
raise Xlsxrb::DecryptionError, "Unsupported hash algorithm: #{name}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def cipher_name_for(algo, key_bits)
|
|
219
|
+
case algo.to_s.upcase
|
|
220
|
+
when "AES"
|
|
221
|
+
"aes-#{key_bits}-cbc"
|
|
222
|
+
else
|
|
223
|
+
raise Xlsxrb::DecryptionError, "Unsupported cipher algorithm: #{algo}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def derive_h_final(digest, password, salt, spin_count)
|
|
228
|
+
pw_utf16 = password.encode("UTF-16LE").b
|
|
229
|
+
h = digest.digest(salt + pw_utf16)
|
|
230
|
+
|
|
231
|
+
spin_count.times do |i|
|
|
232
|
+
h = digest.digest([i].pack("V") + h)
|
|
233
|
+
end
|
|
234
|
+
h
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def derive_key(digest, h_final, block_key, key_bytes)
|
|
238
|
+
digest.digest(h_final + block_key)[0, key_bytes]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def aes_decrypt(cipher_name, key, init_vector, data)
|
|
242
|
+
cipher = OpenSSL::Cipher.new(cipher_name)
|
|
243
|
+
cipher.decrypt
|
|
244
|
+
cipher.key = key
|
|
245
|
+
cipher.iv = init_vector[0, 16]
|
|
246
|
+
cipher.padding = 0
|
|
247
|
+
cipher.update(data) + cipher.final
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def aes_encrypt(cipher_name, key, init_vector, data)
|
|
251
|
+
cipher = OpenSSL::Cipher.new(cipher_name)
|
|
252
|
+
cipher.encrypt
|
|
253
|
+
cipher.key = key
|
|
254
|
+
cipher.iv = init_vector[0, 16]
|
|
255
|
+
cipher.padding = 0
|
|
256
|
+
# Ensure data is multiple of 16 bytes
|
|
257
|
+
if (data.bytesize % 16) != 0
|
|
258
|
+
pad_len = 16 - (data.bytesize % 16)
|
|
259
|
+
data += ("\x00".b * pad_len)
|
|
260
|
+
end
|
|
261
|
+
cipher.update(data) + cipher.final
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def aes_decrypt_block(cipher_name, key, init_vector, data)
|
|
265
|
+
cipher = OpenSSL::Cipher.new(cipher_name)
|
|
266
|
+
cipher.decrypt
|
|
267
|
+
cipher.key = key
|
|
268
|
+
cipher.iv = init_vector[0, 16]
|
|
269
|
+
cipher.padding = 0
|
|
270
|
+
cipher.update(data) + cipher.final
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def aes_encrypt_block(cipher_name, key, init_vector, data)
|
|
274
|
+
cipher = OpenSSL::Cipher.new(cipher_name)
|
|
275
|
+
cipher.encrypt
|
|
276
|
+
cipher.key = key
|
|
277
|
+
cipher.iv = init_vector[0, 16]
|
|
278
|
+
cipher.padding = 0
|
|
279
|
+
cipher.update(data) + cipher.final
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module Xlsxrb
|
|
9
|
+
module Ooxml
|
|
10
|
+
module Crypto
|
|
11
|
+
# Implements Microsoft Office Standard Encryption specified in [MS-OFFCRYPTO] Section 2.3.6.
|
|
12
|
+
class Standard
|
|
13
|
+
CSP_NAME = "Microsoft Enhanced RSA and AES Cryptographic Provider\x00".encode("UTF-16LE").b.freeze
|
|
14
|
+
ITERATION_COUNT = 50_000
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Decrypts a Standard-encrypted package.
|
|
18
|
+
def decrypt(encryption_info_bytes, encrypted_package_bytes, password)
|
|
19
|
+
salt, encrypted_verifier, verifier_hash_size, encrypted_verifier_hash = parse_encryption_info(encryption_info_bytes)
|
|
20
|
+
password_str = password.to_s
|
|
21
|
+
|
|
22
|
+
# Derive key K
|
|
23
|
+
k = derive_key(password_str, salt)
|
|
24
|
+
|
|
25
|
+
# Verify password
|
|
26
|
+
verifier = aes_ecb_decrypt(k, encrypted_verifier)
|
|
27
|
+
expected_hash = OpenSSL::Digest::SHA1.digest(verifier)
|
|
28
|
+
|
|
29
|
+
decrypted_hash = aes_ecb_decrypt(k, encrypted_verifier_hash)[0, verifier_hash_size]
|
|
30
|
+
raise Xlsxrb::InvalidPasswordError, "Standard encryption password verification failed" unless OpenSSL.secure_compare(decrypted_hash, expected_hash)
|
|
31
|
+
|
|
32
|
+
# Decrypt package stream
|
|
33
|
+
raise Xlsxrb::DecryptionError, "Encrypted package stream is too short" if encrypted_package_bytes.bytesize < 8
|
|
34
|
+
|
|
35
|
+
total_size = encrypted_package_bytes[0, 8].unpack1("Q<")
|
|
36
|
+
raise Xlsxrb::DecryptionError, "Encrypted package size is invalid or exceeds limits" if total_size.negative? || total_size > 0x400_000_000
|
|
37
|
+
|
|
38
|
+
encrypted_data = encrypted_package_bytes[8..] || "".b
|
|
39
|
+
|
|
40
|
+
decrypted = aes_ecb_decrypt(k, encrypted_data)
|
|
41
|
+
decrypted[0, total_size] || "".b
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Encrypts plain zip data into Standard Encryption streams.
|
|
45
|
+
def encrypt(plain_bytes, password)
|
|
46
|
+
password_str = password.to_s
|
|
47
|
+
salt = SecureRandom.random_bytes(16)
|
|
48
|
+
verifier = SecureRandom.random_bytes(16)
|
|
49
|
+
|
|
50
|
+
# Derive key K
|
|
51
|
+
k = derive_key(password_str, salt)
|
|
52
|
+
|
|
53
|
+
# Encrypt verifier
|
|
54
|
+
encrypted_verifier = aes_ecb_encrypt(k, verifier)
|
|
55
|
+
|
|
56
|
+
# Encrypt verifier hash
|
|
57
|
+
verifier_hash = OpenSSL::Digest::SHA1.digest(verifier)
|
|
58
|
+
padded_hash = verifier_hash.ljust(32, "\x00".b)
|
|
59
|
+
encrypted_verifier_hash = aes_ecb_encrypt(k, padded_hash)
|
|
60
|
+
|
|
61
|
+
# Build EncryptionInfo Stream
|
|
62
|
+
header_size = 32 + CSP_NAME.bytesize
|
|
63
|
+
info_stream = +""
|
|
64
|
+
# Version & Flags: vMajor=3, vMinor=2, Flags=0x24 (CryptoAPI AES-128)
|
|
65
|
+
info_stream << [3, 2, 0x24].pack("vvV")
|
|
66
|
+
# EncryptionHeader
|
|
67
|
+
info_stream << [header_size].pack("V")
|
|
68
|
+
info_stream << [0x24, 0, 0x0000660E, 0x00008004, 128, 0x00000018, 0, 0].pack("V8")
|
|
69
|
+
info_stream << CSP_NAME
|
|
70
|
+
# EncryptionVerifier
|
|
71
|
+
info_stream << [16].pack("V") # Salt size
|
|
72
|
+
info_stream << salt
|
|
73
|
+
info_stream << encrypted_verifier # 16 bytes
|
|
74
|
+
info_stream << [20].pack("V") # Verifier hash size (SHA-1 = 20)
|
|
75
|
+
info_stream << encrypted_verifier_hash # 32 bytes
|
|
76
|
+
|
|
77
|
+
# Build EncryptedPackage Stream
|
|
78
|
+
padded_plain = plain_bytes
|
|
79
|
+
padded_plain = padded_plain.ljust(padded_plain.bytesize + 16 - (padded_plain.bytesize % 16), "\x00".b) if (padded_plain.bytesize % 16) != 0
|
|
80
|
+
|
|
81
|
+
encrypted_pkg_data = aes_ecb_encrypt(k, padded_plain)
|
|
82
|
+
pkg_stream = [plain_bytes.bytesize].pack("Q<") + encrypted_pkg_data
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
"EncryptionInfo" => info_stream,
|
|
86
|
+
"EncryptedPackage" => pkg_stream
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def parse_encryption_info(bytes)
|
|
93
|
+
raise Xlsxrb::DecryptionError, "EncryptionInfo stream too short" if bytes.bytesize < 40
|
|
94
|
+
|
|
95
|
+
major, minor, = bytes[0, 8].unpack("vvV")
|
|
96
|
+
raise Xlsxrb::DecryptionError, "Unsupported standard encryption version #{major}.#{minor}" unless [2, 3, 4].include?(major) && minor == 2
|
|
97
|
+
|
|
98
|
+
header_size = bytes[8, 4].unpack1("V") || 0
|
|
99
|
+
verifier_offset = 12 + header_size
|
|
100
|
+
raise Xlsxrb::DecryptionError, "EncryptionInfo header offset out of bounds" if verifier_offset + 56 > bytes.bytesize
|
|
101
|
+
|
|
102
|
+
salt_size = bytes[verifier_offset, 4].unpack1("V") || 0
|
|
103
|
+
raise Xlsxrb::DecryptionError, "Invalid salt size in Standard encryption" if salt_size > 64 || (verifier_offset + 4 + salt_size + 48) > bytes.bytesize
|
|
104
|
+
|
|
105
|
+
salt = bytes[verifier_offset + 4, salt_size]
|
|
106
|
+
encrypted_verifier = bytes[verifier_offset + 4 + salt_size, 16]
|
|
107
|
+
|
|
108
|
+
hash_size_offset = verifier_offset + 4 + salt_size + 16
|
|
109
|
+
verifier_hash_size = bytes[hash_size_offset, 4].unpack1("V") || 0
|
|
110
|
+
encrypted_verifier_hash = bytes[hash_size_offset + 4, 32]
|
|
111
|
+
|
|
112
|
+
[salt, encrypted_verifier, verifier_hash_size, encrypted_verifier_hash]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def derive_key(password, salt, block_num = 0)
|
|
116
|
+
pw_utf16 = password.encode("UTF-16LE").b
|
|
117
|
+
h = OpenSSL::Digest::SHA1.digest(salt + pw_utf16)
|
|
118
|
+
|
|
119
|
+
ITERATION_COUNT.times do |i|
|
|
120
|
+
h = OpenSSL::Digest::SHA1.digest([i].pack("V") + h)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
x = OpenSSL::Digest::SHA1.digest(h + [block_num].pack("V"))
|
|
124
|
+
buf1 = x.ljust(64, "\x00".b).bytes.map { |b| b ^ 0x36 }.pack("C*")
|
|
125
|
+
k1 = OpenSSL::Digest::SHA1.digest(buf1)
|
|
126
|
+
|
|
127
|
+
buf2 = x.ljust(64, "\x00".b).bytes.map { |b| b ^ 0x5C }.pack("C*")
|
|
128
|
+
k2 = OpenSSL::Digest::SHA1.digest(buf2)
|
|
129
|
+
|
|
130
|
+
(k1 + k2)[0, 16]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def aes_ecb_encrypt(key, data)
|
|
134
|
+
cipher = OpenSSL::Cipher.new("aes-128-ecb")
|
|
135
|
+
cipher.encrypt
|
|
136
|
+
cipher.key = key
|
|
137
|
+
cipher.padding = 0
|
|
138
|
+
cipher.update(data) + cipher.final
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def aes_ecb_decrypt(key, data)
|
|
142
|
+
cipher = OpenSSL::Cipher.new("aes-128-ecb")
|
|
143
|
+
cipher.decrypt
|
|
144
|
+
cipher.key = key
|
|
145
|
+
cipher.padding = 0
|
|
146
|
+
cipher.update(data) + cipher.final
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "cfb"
|
|
6
|
+
require_relative "crypto/agile"
|
|
7
|
+
require_relative "crypto/standard"
|
|
8
|
+
|
|
9
|
+
module Xlsxrb
|
|
10
|
+
module Ooxml
|
|
11
|
+
# High-level encryption and decryption facade for [MS-OFFCRYPTO] Excel document protection.
|
|
12
|
+
module Crypto
|
|
13
|
+
class << self
|
|
14
|
+
# Checks if the binary data is an encrypted Compound File Binary package.
|
|
15
|
+
def encrypted?(data)
|
|
16
|
+
return false unless Cfb::Reader.cfb?(data)
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
reader = Cfb::Reader.new(data)
|
|
20
|
+
reader.stream_names.any? { |n| n.casecmp?("EncryptionInfo") }
|
|
21
|
+
rescue StandardError
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Decrypts an encrypted XLSX (CFB) package with the given password.
|
|
27
|
+
def decrypt(cfb_data, password)
|
|
28
|
+
raise Xlsxrb::EncryptedFileError, "Password is required to decrypt this file" if password.nil? || password.to_s.empty?
|
|
29
|
+
|
|
30
|
+
reader = Cfb::Reader.new(cfb_data)
|
|
31
|
+
encryption_info = reader.read_stream("EncryptionInfo")
|
|
32
|
+
encrypted_package = reader.read_stream("EncryptedPackage")
|
|
33
|
+
|
|
34
|
+
raise Xlsxrb::DecryptionError, "Missing EncryptionInfo or EncryptedPackage stream" if encryption_info.nil? || encrypted_package.nil?
|
|
35
|
+
|
|
36
|
+
major, minor = encryption_info[0, 4].unpack("vv")
|
|
37
|
+
if major == 4 && minor == 4
|
|
38
|
+
Agile.decrypt(encryption_info, encrypted_package, password)
|
|
39
|
+
elsif minor == 2
|
|
40
|
+
Standard.decrypt(encryption_info, encrypted_package, password)
|
|
41
|
+
else
|
|
42
|
+
raise Xlsxrb::DecryptionError, "Unsupported encryption version #{major}.#{minor}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Encrypts a plain ZIP payload with the given password into an encrypted CFB package.
|
|
47
|
+
def encrypt(plain_zip_data, password, mode: :standard)
|
|
48
|
+
return plain_zip_data if password.nil? || password.to_s.empty?
|
|
49
|
+
|
|
50
|
+
streams = if mode == :agile
|
|
51
|
+
Agile.encrypt(plain_zip_data, password)
|
|
52
|
+
else
|
|
53
|
+
Standard.encrypt(plain_zip_data, password)
|
|
54
|
+
end
|
|
55
|
+
Cfb::Writer.write(streams)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/xlsxrb/stream_row.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Xlsxrb
|
|
|
52
52
|
# @return [Enumerator, void]
|
|
53
53
|
# @api public
|
|
54
54
|
#: () { (Elements::Cell) -> void } -> void
|
|
55
|
-
#:
|
|
55
|
+
#: () -> Enumerator[Elements::Cell, void]
|
|
56
56
|
def each_cell(&)
|
|
57
57
|
return enum_for(:each_cell) unless block_given?
|
|
58
58
|
|
|
@@ -70,7 +70,7 @@ module Xlsxrb
|
|
|
70
70
|
# @return [Enumerator, void]
|
|
71
71
|
# @api public
|
|
72
72
|
#: () { (Elements::Cell) -> void } -> void
|
|
73
|
-
#:
|
|
73
|
+
#: () -> Enumerator[Elements::Cell, void]
|
|
74
74
|
def each(&)
|
|
75
75
|
each_cell(&)
|
|
76
76
|
end
|
data/lib/xlsxrb/version.rb
CHANGED