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.
@@ -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
@@ -52,7 +52,7 @@ module Xlsxrb
52
52
  # @return [Enumerator, void]
53
53
  # @api public
54
54
  #: () { (Elements::Cell) -> void } -> void
55
- #: | () -> Enumerator[Elements::Cell, void]
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
- #: | () -> Enumerator[Elements::Cell, void]
73
+ #: () -> Enumerator[Elements::Cell, void]
74
74
  def each(&)
75
75
  each_cell(&)
76
76
  end
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module Xlsxrb
6
- VERSION = "0.1.8"
6
+ VERSION = "0.1.9"
7
7
  end