xlsxrb 0.1.7 → 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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/README.md +125 -36
  4. data/Rakefile +36 -7
  5. data/benchmark.rb +32 -4
  6. data/docs/ARCHITECTURE.md +32 -30
  7. data/docs/DEVELOPMENT.md +11 -3
  8. data/docs/QUALITY_ASSURANCE.md +3 -4
  9. data/docs/SPEC_SOURCES.md +7 -1
  10. data/docs/visual/VisualGallery.md +157 -157
  11. data/docs/wasm/ruby.wasm +0 -0
  12. data/lib/ruby_lsp/xlsxrb/addon.rb +1 -1
  13. data/lib/ruby_lsp/xlsxrb/completion_listener.rb +11 -13
  14. data/lib/xlsxrb/elements/coordinate_access.rb +101 -0
  15. data/lib/xlsxrb/elements/row.rb +2 -2
  16. data/lib/xlsxrb/elements/workbook.rb +20 -2
  17. data/lib/xlsxrb/elements/worksheet.rb +94 -124
  18. data/lib/xlsxrb/elements.rb +1 -0
  19. data/lib/xlsxrb/ooxml/cfb.rb +490 -0
  20. data/lib/xlsxrb/ooxml/crypto/agile.rb +285 -0
  21. data/lib/xlsxrb/ooxml/crypto/standard.rb +152 -0
  22. data/lib/xlsxrb/ooxml/crypto.rb +60 -0
  23. data/lib/xlsxrb/ooxml/worksheet_parser.rb +21 -15
  24. data/lib/xlsxrb/stream_row.rb +181 -0
  25. data/lib/xlsxrb/version.rb +1 -1
  26. data/lib/xlsxrb.rb +271 -122
  27. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +70 -0
  28. data/sig/generated/xlsxrb/elements/worksheet.rbs +119 -14
  29. data/sig/generated/xlsxrb/ooxml/cfb.rbs +121 -0
  30. data/sig/generated/xlsxrb/ooxml/crypto/agile.rbs +46 -0
  31. data/sig/generated/xlsxrb/ooxml/crypto/standard.rbs +28 -0
  32. data/sig/generated/xlsxrb/ooxml/crypto.rbs +17 -0
  33. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +2 -0
  34. data/sig/generated/xlsxrb/stream_row.rbs +127 -0
  35. data/sig/generated/xlsxrb.rbs +135 -71
  36. data/vendor/sdk_runner/Program.cs +3 -1
  37. metadata +13 -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
@@ -515,17 +515,17 @@ module Xlsxrb
515
515
  row_end = xml.index("</row>", tag_end + 1)
516
516
  break unless row_end
517
517
 
518
- row_source = { part: part_name, row: row_index }
519
- cells = fast_parse_cells_direct(xml, tag_end + 1, row_end, shared_strings, row_source)
520
-
521
- row_obj = Elements::Row.new(
518
+ { part: part_name, row: row_index }
519
+ row_obj = StreamRow.new(
522
520
  index: row_index,
523
- cells: cells,
521
+ xml_bytes: xml,
522
+ from: tag_end + 1,
523
+ to: row_end,
524
+ shared_strings: shared_strings,
524
525
  height: attrs[:height],
525
526
  hidden: attrs[:hidden] || false,
526
527
  custom_height: attrs[:custom_height] || false,
527
- outline_level: attrs[:outline_level],
528
- errors: Elements::EMPTY_ERRORS
528
+ outline_level: attrs[:outline_level]
529
529
  )
530
530
  block.call(row_obj)
531
531
 
@@ -537,7 +537,13 @@ module Xlsxrb
537
537
 
538
538
  def self.fast_parse_cells_direct(xml, from, to, shared_strings, row_source)
539
539
  cells = []
540
+ fast_scan_cells_direct(xml, from, to, shared_strings, row_source) { |c| cells << c }
541
+ cells
542
+ end
543
+
544
+ def self.fast_scan_cells_direct(xml, from, to, shared_strings, row_source, &block)
540
545
  pos = from
546
+ cell_count = 0
541
547
 
542
548
  while pos < to
543
549
  c_start = xml.index("<c", pos)
@@ -589,7 +595,6 @@ module Xlsxrb
589
595
 
590
596
  row_idx = (row_idx * 10) + (cb - 48)
591
597
  ai += 1
592
-
593
598
  end
594
599
  row_idx -= 1
595
600
  ai += 1 if xml.getbyte(ai) == 34
@@ -619,7 +624,6 @@ module Xlsxrb
619
624
 
620
625
  style_index = (style_index * 10) + (cb - 48)
621
626
  ai += 1
622
-
623
627
  end
624
628
  ai += 1 if xml.getbyte(ai) == 34
625
629
  else
@@ -629,13 +633,15 @@ module Xlsxrb
629
633
 
630
634
  # Self-closing <c ... />
631
635
  if xml.getbyte(c_tag_end - 1) == 47
632
- cells << Elements::Cell.new(
636
+ cell_obj = Elements::Cell.new(
633
637
  row_index: row_idx || row_source[:row],
634
- column_index: col_idx || cells.size,
638
+ column_index: col_idx || cell_count,
635
639
  value: nil,
636
640
  style_index: style_index,
637
641
  errors: Elements::EMPTY_ERRORS
638
642
  )
643
+ cell_count += 1
644
+ block.call(cell_obj)
639
645
  pos = c_tag_end + 1
640
646
  next
641
647
  end
@@ -721,19 +727,19 @@ module Xlsxrb
721
727
  end
722
728
 
723
729
  val_to_use = inline_str || value
724
- cells << Elements::Cell.new(
730
+ cell_obj = Elements::Cell.new(
725
731
  row_index: row_idx || row_source[:row],
726
- column_index: col_idx || cells.size,
732
+ column_index: col_idx || cell_count,
727
733
  value: val_to_use,
728
734
  formula: formula,
729
735
  style_index: style_index,
730
736
  errors: Elements::EMPTY_ERRORS
731
737
  )
738
+ cell_count += 1
739
+ block.call(cell_obj)
732
740
 
733
741
  pos = c_end + 4
734
742
  end
735
-
736
- cells
737
743
  end
738
744
 
739
745
  private_class_method :fast_parse_cells_direct