unxls 0.0.1
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 +7 -0
- data/ext/extconf.rb +4 -0
- data/lib/formulas/constants.rb +886 -0
- data/lib/formulas/map.rb +5 -0
- data/lib/formulas/parsed_expressions.rb +229 -0
- data/lib/formulas/record.rb +130 -0
- data/lib/formulas/structure.rb +30 -0
- data/lib/unxls.rb +16 -0
- data/lib/unxls/biff8/browser.rb +347 -0
- data/lib/unxls/biff8/constants.rb +624 -0
- data/lib/unxls/biff8/record.rb +1267 -0
- data/lib/unxls/biff8/structure.rb +2740 -0
- data/lib/unxls/biff8/workbook_stream.rb +322 -0
- data/lib/unxls/bit_ops.rb +92 -0
- data/lib/unxls/dtyp.rb +33 -0
- data/lib/unxls/log.rb +59 -0
- data/lib/unxls/map.rb +59 -0
- data/lib/unxls/offcrypto.rb +423 -0
- data/lib/unxls/oshared.rb +210 -0
- data/lib/unxls/parser.rb +100 -0
- data/lib/unxls/version.rb +5 -0
- data/spec/bitops_spec.rb +95 -0
- data/spec/spec_helper.rb +117 -0
- data/spec/unxls_spec.rb +2651 -0
- metadata +167 -0
data/lib/unxls/map.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ole/storage'
|
4
|
+
require 'openssl'
|
5
|
+
require 'set'
|
6
|
+
require 'zip'
|
7
|
+
require 'awesome_print' if $DEBUG
|
8
|
+
require 'pry' if $DEBUG
|
9
|
+
|
10
|
+
module Unxls
|
11
|
+
module Log; end # For easy output of values in different formats
|
12
|
+
class BitOps; end # For easy binary operations
|
13
|
+
|
14
|
+
module Helpers # Too small yet to move to a separate file
|
15
|
+
refine Object do
|
16
|
+
# Convert String to StringIO unless it's already StringIO
|
17
|
+
# @return [StringIO]
|
18
|
+
# @raises [RuntimeError] Unless receiver is a String or a StringIO
|
19
|
+
def to_sio
|
20
|
+
raise "Cannot convert #{self.class} to StringIO" unless self.kind_of?(StringIO) || self.instance_of?(String)
|
21
|
+
self.is_a?(StringIO) ? self : StringIO.new(self)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Oshared; end # [MS-OSHARED]: Office Common Data Types and Objects Structures
|
27
|
+
module Dtyp; end # [MS-DTYP]: Windows Data Types
|
28
|
+
module Offcrypto; end # [MS-OFFCRYPTO]: Office Document Cryptography Structure
|
29
|
+
|
30
|
+
module Biff8 # Group code by BIFF version
|
31
|
+
module Constants; end # Large constants
|
32
|
+
|
33
|
+
class WorkbookStream; end # Deals with Workbook Stream (2.1.7.20)
|
34
|
+
|
35
|
+
class Record; end # Recipes for parsing of particular records (2.4)
|
36
|
+
|
37
|
+
module Structure; end # Deals with structures used in records (2.5)
|
38
|
+
module ParsedExpressions; end # Extends Structure. Deals with structures used in formulas (2.5.198). Defined in Structure's file for now.
|
39
|
+
|
40
|
+
class Browser; end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Parser; end # Opens the compound file and parses the needed storages and streams (2.1.7)
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
require_relative 'version' unless defined?(Unxls::VERSION)
|
48
|
+
require_relative 'log'
|
49
|
+
require_relative 'bit_ops'
|
50
|
+
# require_relative '../../ext/bit_ops.so'
|
51
|
+
require_relative 'biff8/constants'
|
52
|
+
require_relative 'biff8/workbook_stream'
|
53
|
+
require_relative 'biff8/record'
|
54
|
+
require_relative 'biff8/structure'
|
55
|
+
require_relative 'biff8/browser'
|
56
|
+
require_relative 'oshared'
|
57
|
+
require_relative 'dtyp'
|
58
|
+
require_relative 'offcrypto'
|
59
|
+
require_relative 'parser'
|
@@ -0,0 +1,423 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# [MS-OFFCRYPTO]: Office Document Cryptography Structure
|
4
|
+
module Unxls::Offcrypto
|
5
|
+
extend self
|
6
|
+
|
7
|
+
BLOCK_SIZE = 1024
|
8
|
+
|
9
|
+
DEFAULT_PASSWORD = 'VelvetSweatshop'
|
10
|
+
|
11
|
+
#
|
12
|
+
# XOR obfuscation
|
13
|
+
#
|
14
|
+
|
15
|
+
XOR_PAD = [0xBB, 0xFF, 0xFF, 0xBA, 0xFF, 0xFF, 0xB9, 0x80, 0x00, 0xBE, 0x0F, 0x00, 0xBF, 0x0F, 0x00]
|
16
|
+
|
17
|
+
XOR_INITIAL_CODE = [
|
18
|
+
0xE1F0, 0x1D0F, 0xCC9C, 0x84C0, 0x110C,
|
19
|
+
0x0E10, 0xF1CE, 0x313E, 0x1872, 0xE139,
|
20
|
+
0xD40F, 0x84F9, 0x280C, 0xA96A, 0x4EC3
|
21
|
+
]
|
22
|
+
|
23
|
+
XOR_MATRIX = [
|
24
|
+
0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09,
|
25
|
+
0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF,
|
26
|
+
0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0,
|
27
|
+
0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40,
|
28
|
+
0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5,
|
29
|
+
0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A,
|
30
|
+
0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9,
|
31
|
+
0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0,
|
32
|
+
0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC,
|
33
|
+
0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10,
|
34
|
+
0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168,
|
35
|
+
0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C,
|
36
|
+
0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD,
|
37
|
+
0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC,
|
38
|
+
0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4
|
39
|
+
]
|
40
|
+
|
41
|
+
# 2.3.7.2 Binary Document XOR Array Initialization Method 1
|
42
|
+
# FUNCTION CreateXorKey_Method1
|
43
|
+
# @param password [String]
|
44
|
+
# @return [Integer]
|
45
|
+
def _create_xor_key_method1(password)
|
46
|
+
ansi_password = _unicode_password_to_ansi(password)
|
47
|
+
|
48
|
+
xor_key = XOR_INITIAL_CODE[ansi_password.size - 1]
|
49
|
+
current_element = 0x68
|
50
|
+
|
51
|
+
ansi_password.bytes.reverse.each do |c|
|
52
|
+
7.times do
|
53
|
+
xor_key ^= XOR_MATRIX[current_element] unless (c & 0x40).zero?
|
54
|
+
c <<= 1
|
55
|
+
current_element -= 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
xor_key
|
60
|
+
end
|
61
|
+
|
62
|
+
# 2.3.7.2 Binary Document XOR Array Initialization Method 1
|
63
|
+
# FUNCTION CreateXorArray_Method1
|
64
|
+
# @param password [String]
|
65
|
+
# @return [Array<Integer>]
|
66
|
+
def _create_xor_array_method1(password)
|
67
|
+
ansi_password = _unicode_password_to_ansi(password).bytes
|
68
|
+
xor_key = _create_xor_key_method1(password)
|
69
|
+
index = password.size
|
70
|
+
obfuscation_array = Array.new(16) { 0 }
|
71
|
+
|
72
|
+
xor_key_high = xor_key >> 8
|
73
|
+
xor_key_low = xor_key & 0xFF
|
74
|
+
|
75
|
+
if (index & 1) == 1
|
76
|
+
obfuscation_array[index] = _xor_ror(XOR_PAD[0], xor_key_high)
|
77
|
+
index -= 1
|
78
|
+
obfuscation_array[index] = _xor_ror(ansi_password[-1], xor_key_low)
|
79
|
+
end
|
80
|
+
|
81
|
+
while index > 0
|
82
|
+
index -= 1
|
83
|
+
obfuscation_array[index] = _xor_ror(ansi_password[index], xor_key_high)
|
84
|
+
index -= 1
|
85
|
+
obfuscation_array[index] = _xor_ror(ansi_password[index], xor_key_low)
|
86
|
+
end
|
87
|
+
|
88
|
+
index = 15
|
89
|
+
pad_index = 15 - password.size
|
90
|
+
while pad_index > 0
|
91
|
+
obfuscation_array[index] = _xor_ror(XOR_PAD[pad_index], xor_key_high)
|
92
|
+
index -= 1
|
93
|
+
pad_index -= 1
|
94
|
+
obfuscation_array[index] = _xor_ror(XOR_PAD[pad_index], xor_key_low)
|
95
|
+
index -= 1
|
96
|
+
pad_index -= 1
|
97
|
+
end
|
98
|
+
|
99
|
+
obfuscation_array
|
100
|
+
end
|
101
|
+
|
102
|
+
# 2.3.7.1 Binary Document Password Verifier Derivation Method 1
|
103
|
+
# FUNCTION CreatePasswordVerifier_Method1
|
104
|
+
# @param password [String]
|
105
|
+
# @return [Integer]
|
106
|
+
def _xor_password_verifier_method1(password)
|
107
|
+
verifier = 0
|
108
|
+
ansi_password = _unicode_password_to_ansi(password)
|
109
|
+
|
110
|
+
ansi_password.bytes.unshift(ansi_password.size).reverse.each do |b|
|
111
|
+
int1 = (verifier & 0b0100_0000_0000_0000).zero? ? 0 : 1
|
112
|
+
int2 = (verifier << 1) & 0b0111_1111_1111_1111
|
113
|
+
int3 = int1 | int2
|
114
|
+
verifier = int3 ^ b
|
115
|
+
end
|
116
|
+
|
117
|
+
verifier ^ 0xCE4B
|
118
|
+
end
|
119
|
+
|
120
|
+
# @param password [String]
|
121
|
+
# @param verification_bytes [String]
|
122
|
+
def _xor_password_match?(password, verification_bytes)
|
123
|
+
_xor_password_verifier_method1(password) == verification_bytes
|
124
|
+
end
|
125
|
+
|
126
|
+
# See 2.3.7.4 Binary Document Password Verifier Derivation Method 2 (p. 61)
|
127
|
+
# @note This method works reliably only for passwords consisting of ASCII characters
|
128
|
+
# @param password [String]
|
129
|
+
# @return [String]
|
130
|
+
def _unicode_password_to_ansi(password)
|
131
|
+
password.encode(Encoding::UTF_16LE).unpack('v*').map do |c|
|
132
|
+
low_byte = c & 0xFF
|
133
|
+
low_byte.zero? ? ((c >> 8) & 0xFF) : low_byte
|
134
|
+
end.pack('C*')
|
135
|
+
end
|
136
|
+
|
137
|
+
def _xor_ror(byte1, byte2)
|
138
|
+
Unxls::BitOps.new(byte1 ^ byte2).ror(8, 1)
|
139
|
+
end
|
140
|
+
|
141
|
+
# https://github.com/apache/poi/blob/trunk/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java#L445
|
142
|
+
# https://github.com/apache/poi/blob/trunk/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java
|
143
|
+
# @param byte [Integer]
|
144
|
+
# @param byte_index [Integer]
|
145
|
+
# @param xor_decryption_array [Array<Integer>]
|
146
|
+
# @param record_offset [Integer]
|
147
|
+
# @param record_size [Integer]
|
148
|
+
# @return [String]
|
149
|
+
def _xor_decrypt_byte(byte, byte_index, xor_decryption_array, record_offset, record_size)
|
150
|
+
# "The initial value for XorArrayIndex is as follows:
|
151
|
+
# XorArrayIndex = (FileOffset + Data.Length) % 16
|
152
|
+
# The FileOffset variable in this context is the stream offset into the Workbook stream at
|
153
|
+
# the time we are about to write each of the bytes of the record data.
|
154
|
+
# This (the value) is then incremented after each byte is written."
|
155
|
+
# From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5
|
156
|
+
xor_array_index = (record_offset + Unxls::Biff8::Record::HEADER_SIZE + record_size + byte_index) & 0xF
|
157
|
+
|
158
|
+
byte ^= xor_decryption_array[xor_array_index]
|
159
|
+
byte = ((byte << 3) | (byte >> 5)) & 0xFF # rotate right 5 bits
|
160
|
+
|
161
|
+
[byte].pack('C')
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# RC4
|
166
|
+
#
|
167
|
+
|
168
|
+
# 2.1.4 Version
|
169
|
+
# The Version structure specifies the version of a product or feature. It contains a major and a minor version number.
|
170
|
+
# @param data [String]
|
171
|
+
# @return [Hash]
|
172
|
+
def version(data)
|
173
|
+
v_major, v_minor = data.unpack('vv')
|
174
|
+
|
175
|
+
{
|
176
|
+
vMajor: v_major, # vMajor (2 bytes): An unsigned integer that specifies the major version number.
|
177
|
+
vMinor: v_minor # vMinor (2 bytes): An unsigned integer that specifies the minor version number.
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
# 2.3.6.1 RC4 Encryption Header
|
182
|
+
# @param io [StringIO]
|
183
|
+
# @return [Hash]
|
184
|
+
def rc4encryptionheader(io)
|
185
|
+
{
|
186
|
+
EncryptionVersionInfo: version(io.read(4)), # EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where Version.vMajor MUST be 0x0001 and Version.vMinor MUST be 0x0001.
|
187
|
+
Salt: io.read(16), # Salt (16 bytes): A randomly generated array of bytes that specifies the salt value used during password hash generation.
|
188
|
+
EncryptedVerifier: io.read(16), # EncryptedVerifier (16 bytes): An additional 16-byte verifier encrypted using a 40-bit RC4 cipher initialized as specified in section 2.3.6.2, with a block number of 0x00000000.
|
189
|
+
EncryptedVerifierHash: io.read(16) # EncryptedVerifierHash (16 bytes): A 40-bit RC4 encrypted MD5 hash of the verifier used to generate the EncryptedVerifier field.
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
# 2.3.6.2 Encryption Key Derivation
|
194
|
+
# @param password [String]
|
195
|
+
# @param salt [String]
|
196
|
+
# @param block_num [Integer]
|
197
|
+
# @return [String]
|
198
|
+
def _rc4_make_key(password, salt, block_num)
|
199
|
+
h0 = OpenSSL::Digest::MD5.digest(password.encode(Encoding::UTF_16LE))
|
200
|
+
buffer = (h0[0..4] + salt) * 16
|
201
|
+
h1 = OpenSSL::Digest::MD5.digest(buffer)
|
202
|
+
hfin = "#{h1[0..4]}#{[block_num].pack('V')}"
|
203
|
+
OpenSSL::Digest::MD5.digest(hfin)
|
204
|
+
end
|
205
|
+
|
206
|
+
# 2.3.6.4 Password Verification
|
207
|
+
# @param key [String]
|
208
|
+
# @param encr_verifier [String]
|
209
|
+
# @param encr_verifier_hash [String]
|
210
|
+
def _rc4_password_match?(key, encr_verifier, encr_verifier_hash)
|
211
|
+
cipher = OpenSSL::Cipher::RC4.new
|
212
|
+
cipher.decrypt
|
213
|
+
cipher.key = key
|
214
|
+
decrypted_verifier = cipher.update(encr_verifier) + cipher.final
|
215
|
+
decrypted_verifier_hash = cipher.update(encr_verifier_hash) + cipher.final
|
216
|
+
hashed_verifier = OpenSSL::Digest::MD5.digest(decrypted_verifier)
|
217
|
+
hashed_verifier == decrypted_verifier_hash
|
218
|
+
end
|
219
|
+
|
220
|
+
# @param data [String]
|
221
|
+
# @param key [String]
|
222
|
+
# @return [String]
|
223
|
+
def _rc4_decrypt(data, key)
|
224
|
+
cipher = OpenSSL::Cipher::RC4.new
|
225
|
+
cipher.decrypt
|
226
|
+
cipher.key = key
|
227
|
+
cipher.update(data) + cipher.final
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# CryptoAPI
|
232
|
+
#
|
233
|
+
|
234
|
+
# 2.3.5.1 RC4 CryptoAPI Encryption Header
|
235
|
+
# @param io [StringIO]
|
236
|
+
# @return [Hash]
|
237
|
+
def rc4cryptoapiheader(io)
|
238
|
+
result = {
|
239
|
+
EncryptionVersionInfo: version(io.read(4)), # EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4) that specifies the encryption version used to create the document and the encryption version required to open the document.
|
240
|
+
EncryptionHeaderFlags: encryptionheaderflags(io.read(4).unpack('V').first) # EncryptionHeader.Flags (4 bytes): A copy of the Flags stored in the EncryptionHeader structure (section 2.3.2) that is stored in this stream.
|
241
|
+
}
|
242
|
+
|
243
|
+
encryption_header_size = io.read(4).unpack('V').first
|
244
|
+
result[:EncryptionHeaderSize] = encryption_header_size # EncryptionHeaderSize (4 bytes): An unsigned integer that specifies the size, in bytes, of the EncryptionHeader structure.
|
245
|
+
result[:EncryptionHeader] = encryptionheader(io) # EncryptionHeader (variable): An EncryptionHeader structure (section 2.3.2) used to encrypt the structure.
|
246
|
+
result[:EncryptionVerifier] = encryptionverifier(io) # EncryptionVerifier (variable): An EncryptionVerifier structure as specified in section 2.3.3 that is generated as specified in section 2.3.5.5.
|
247
|
+
result[:_encryption_algorithm] = _rc4cryptoapi_encryption_alg(result[:EncryptionHeader])
|
248
|
+
result[:_hashing_algorithm] = _rc4cryptoapi_hashing_alg(result[:EncryptionHeader])
|
249
|
+
|
250
|
+
result
|
251
|
+
end
|
252
|
+
|
253
|
+
# 2.3.2 EncryptionHeader
|
254
|
+
# The EncryptionHeader structure is used by ECMA-376 document encryption [ECMA-376] and Office binary document RC4 CryptoAPI encryption, as defined in section 2.3.5, to specify encryption properties for an encrypted stream.
|
255
|
+
# @param io [StringIO]
|
256
|
+
# @return [Hash]
|
257
|
+
def encryptionheader(io)
|
258
|
+
result = {
|
259
|
+
Flags: encryptionheaderflags(io.read(4).unpack('V').first), # Flags (4 bytes): An EncryptionHeaderFlags structure, as specified in section 2.3.1, that specifies properties of the encryption algorithm used.
|
260
|
+
SizeExtra: io.read(4).unpack('V').first, # SizeExtra (4 bytes): A field that is reserved and for which the value MUST be 0x00000000.
|
261
|
+
}
|
262
|
+
|
263
|
+
alg_id = io.read(4).unpack('l<').first # AlgID (4 bytes): A signed integer that specifies the encryption algorithm.
|
264
|
+
result[:AlgID] = alg_id
|
265
|
+
result[:AlgID_d] = {
|
266
|
+
0x0000 => :'Determined by Flags',
|
267
|
+
0x6801 => :RC4,
|
268
|
+
0x660E => :AES128,
|
269
|
+
0x660F => :AES192,
|
270
|
+
0x6610 => :AES256
|
271
|
+
}[alg_id]
|
272
|
+
|
273
|
+
alg_id_hash, key_size, provider_type, _, _ = io.read(20).unpack('l<VV')
|
274
|
+
result[:AlgIDHash] = alg_id_hash # AlgIDHash (4 bytes): A signed integer that specifies the hashing algorithm together with the Flags.fExternal bit.
|
275
|
+
result[:KeySize] = key_size # KeySize (4 bytes): An unsigned integer that specifies the number of bits in the encryption key.
|
276
|
+
result[:ProviderType] = provider_type # ProviderType (4 bytes): An implementation-specific value that corresponds to constants accepted by the specified CSP.
|
277
|
+
# Reserved1 (4 bytes): A value that is undefined and MUST be ignored.
|
278
|
+
# Reserved2 (4 bytes): A value that MUST be 0x00000000 and MUST be ignored.
|
279
|
+
|
280
|
+
result[:CSPName] = Unxls::Oshared._db_zero_terminated(io) # CSPName (variable): A null-terminated Unicode string that specifies the CSP name.
|
281
|
+
|
282
|
+
result
|
283
|
+
end
|
284
|
+
|
285
|
+
# 2.3.1 EncryptionHeaderFlags
|
286
|
+
# The EncryptionHeaderFlags structure specifies properties of the encryption algorithm used.
|
287
|
+
# @param value [Integer]
|
288
|
+
# @return [Hash]
|
289
|
+
def encryptionheaderflags(value)
|
290
|
+
attrs = Unxls::BitOps.new(value)
|
291
|
+
|
292
|
+
{
|
293
|
+
# A – Reserved1 (1 bit): A value that MUST be 0 and MUST be ignored.
|
294
|
+
# B – Reserved2 (1 bit): A value that MUST be 0 and MUST be ignored.
|
295
|
+
fCryptoAPI: attrs.set_at?(2), # C – fCryptoAPI (1 bit): A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption [ECMA- 376] is used.
|
296
|
+
fDocProps: attrs.set_at?(3), # D – fDocProps (1 bit): A value that MUST be 0 if document properties are encrypted. The encryption of document properties is specified in section 2.3.5.4.
|
297
|
+
fExternal: attrs.set_at?(4), # E – fExternal (1 bit): A value that MUST be 1 if extensible encryption is used.
|
298
|
+
fAES: attrs.set_at?(5), # F – fAES (1 bit): A value that MUST be 1 if the protected content is an ECMA-376 document [ECMA- 376]
|
299
|
+
# Unused (26 bits): A value that is undefined and MUST be ignored.
|
300
|
+
}
|
301
|
+
end
|
302
|
+
|
303
|
+
# 2.3.3 EncryptionVerifier
|
304
|
+
# @param io [StringIO]
|
305
|
+
# @return [Hash]
|
306
|
+
def encryptionverifier(io)
|
307
|
+
{
|
308
|
+
SaltSize: io.read(4).unpack('V').first, # SaltSize (4 bytes): An unsigned integer that specifies the size of the Salt field.
|
309
|
+
Salt: io.read(16), # Salt (16 bytes): An array of bytes that specifies the salt value used during password hash generation.
|
310
|
+
EncryptedVerifier: io.read(16), # EncryptedVerifier (16 bytes): A value that MUST be the randomly generated Verifier value encrypted using the algorithm chosen by the implementation.
|
311
|
+
VerifierHashSize: io.read(4).unpack('V').first, # VerifierHashSize (4 bytes): An unsigned integer that specifies the number of bytes needed to contain the hash of the data used to generate the EncryptedVerifier field.
|
312
|
+
EncryptedVerifierHash: io.read # EncryptedVerifierHash (variable): An array of bytes that contains the encrypted form of the hash of the randomly generated Verifier value.
|
313
|
+
}
|
314
|
+
end
|
315
|
+
|
316
|
+
# See AlgIDHash description, p.33
|
317
|
+
# @param encryption_header [Hash]
|
318
|
+
# @return [Symbol]
|
319
|
+
def _rc4cryptoapi_hashing_alg(encryption_header)
|
320
|
+
combination = [
|
321
|
+
encryption_header[:AlgIDHash],
|
322
|
+
encryption_header[:Flags][:fExternal]
|
323
|
+
]
|
324
|
+
|
325
|
+
case combination
|
326
|
+
when [0x0000, true] then :'Determined by the application'
|
327
|
+
|
328
|
+
when [0x0000, false],
|
329
|
+
[0x8004, false] then :SHA1
|
330
|
+
|
331
|
+
else raise "Unknown Flags and AlgIDHash combination: #{combination}"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# See AlgID description, p.33
|
336
|
+
# @param encryption_header [Hash]
|
337
|
+
# @return [Symbol]
|
338
|
+
def _rc4cryptoapi_encryption_alg(encryption_header)
|
339
|
+
combination = [
|
340
|
+
encryption_header[:Flags][:fCryptoAPI],
|
341
|
+
encryption_header[:Flags][:fAES],
|
342
|
+
encryption_header[:Flags][:fExternal],
|
343
|
+
encryption_header[:AlgID]
|
344
|
+
]
|
345
|
+
|
346
|
+
case combination
|
347
|
+
when [false, false, true, 0x0000] then :'Determined by the application'
|
348
|
+
|
349
|
+
when [true, false, false, 0x0000],
|
350
|
+
[true, false, false, 0x6801] then :RC4
|
351
|
+
|
352
|
+
when [true, true, false, 0x0000],
|
353
|
+
[true, true, false, 0x660E] then :'AES-128-CBC'
|
354
|
+
|
355
|
+
when [true, true, false, 0x660F] then :'AES-192-CBC'
|
356
|
+
|
357
|
+
when [true, true, false, 0x6610] then :'AES-256-CBC'
|
358
|
+
|
359
|
+
else raise "Unknown Flags and AlgID combination: #{combination}"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# 2.3.5.2 RC4 CryptoAPI Encryption Key Generation
|
364
|
+
# @param password [String]
|
365
|
+
# @param salt [String]
|
366
|
+
# @param block_num [Integer]
|
367
|
+
# @param key_size [Integer]
|
368
|
+
# @param hash_alg [Symbol]
|
369
|
+
# @return [String]
|
370
|
+
def _rc4cryptoapi_make_key(password, salt, block_num, key_size, hash_alg)
|
371
|
+
h0_digest = _openssl_obj(:Digest, hash_alg)
|
372
|
+
h0_digest.update(salt)
|
373
|
+
h0_digest.update(password.encode(Encoding::UTF_16LE))
|
374
|
+
h0 = h0_digest.digest
|
375
|
+
|
376
|
+
h_digest = _openssl_obj(:Digest, hash_alg)
|
377
|
+
h_digest.update(h0)
|
378
|
+
h_digest.update([block_num].pack('V'))
|
379
|
+
h = h_digest.digest
|
380
|
+
|
381
|
+
h_fin = h[0..(key_size - 1)]
|
382
|
+
h_fin << ("\x00" * (16 - key_size)) if key_size < 16
|
383
|
+
|
384
|
+
h_fin
|
385
|
+
end
|
386
|
+
|
387
|
+
# 2.3.5.6 Password Verification
|
388
|
+
# @param key [String]
|
389
|
+
# @param encr_verifier [String]
|
390
|
+
# @param encr_verifier_hash [String]
|
391
|
+
# @param verifier_hash_size [Integer]
|
392
|
+
# @param hash_alg [Symbol]
|
393
|
+
# @param encr_alg [Symbol]
|
394
|
+
def _rc4cryptoapi_password_match?(key, encr_verifier, encr_verifier_hash, verifier_hash_size, hash_alg, encr_alg)
|
395
|
+
cipher = _openssl_obj(:Cipher, encr_alg)
|
396
|
+
cipher.decrypt
|
397
|
+
cipher.key = key
|
398
|
+
decrypted_verifier = cipher.update(encr_verifier) + cipher.final
|
399
|
+
decrypted_verifier_hash = cipher.update(encr_verifier_hash) + cipher.final
|
400
|
+
decrypted_verifier_hash = decrypted_verifier_hash[0..(verifier_hash_size - 1)]
|
401
|
+
hashed_verifier = _openssl_obj(:Digest, hash_alg).digest(decrypted_verifier)
|
402
|
+
hashed_verifier == decrypted_verifier_hash
|
403
|
+
end
|
404
|
+
|
405
|
+
# @param data [String]
|
406
|
+
# @param key [String]
|
407
|
+
# @param encr_alg [Symbol]
|
408
|
+
# @return [String]
|
409
|
+
def _rc4cryptoapi_decrypt(data, key, encr_alg)
|
410
|
+
cipher = _openssl_obj(:Cipher, encr_alg)
|
411
|
+
cipher.decrypt
|
412
|
+
cipher.key = key
|
413
|
+
cipher.update(data) + cipher.final
|
414
|
+
end
|
415
|
+
|
416
|
+
# @param type [Symbol] e.g. :Cipher, :Digest
|
417
|
+
# @param name [Symbol] e.g. :RC4, SHA1
|
418
|
+
# @return [OpenSSL::Cipher, OpenSSL::Digest] e.g. OpenSSL::Cipher::RC4 instance
|
419
|
+
def _openssl_obj(type, name)
|
420
|
+
Module.class_eval("OpenSSL::#{type}").new(name.to_s)
|
421
|
+
end
|
422
|
+
|
423
|
+
end
|