Module: Unxls::Offcrypto

Extended by:
Offcrypto
Included in:
Offcrypto
Defined in:
lib/unxls/map.rb,
lib/unxls/offcrypto.rb

Overview

[MS-OFFCRYPTO]: Office Document Cryptography Structure

Constant Summary collapse

BLOCK_SIZE =
1024
DEFAULT_PASSWORD =
'VelvetSweatshop'
XOR_PAD =

XOR obfuscation

[0xBB, 0xFF, 0xFF, 0xBA, 0xFF, 0xFF, 0xB9, 0x80, 0x00, 0xBE, 0x0F, 0x00, 0xBF, 0x0F, 0x00]
XOR_INITIAL_CODE =
[
  0xE1F0, 0x1D0F, 0xCC9C, 0x84C0, 0x110C,
  0x0E10, 0xF1CE, 0x313E, 0x1872, 0xE139,
  0xD40F, 0x84F9, 0x280C, 0xA96A, 0x4EC3
]
XOR_MATRIX =
[
  0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09,
  0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF,
  0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0,
  0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40,
  0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5,
  0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A,
  0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9,
  0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0,
  0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC,
  0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10,
  0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168,
  0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C,
  0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD,
  0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC,
  0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4
]

Instance Method Summary collapse

Instance Method Details

#_create_xor_array_method1(password) ⇒ Array<Integer>

2.3.7.2 Binary Document XOR Array Initialization Method 1 FUNCTION CreateXorArray_Method1

Parameters:

  • password (String)

Returns:

  • (Array<Integer>)


66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/unxls/offcrypto.rb', line 66

def _create_xor_array_method1(password)
  ansi_password = _unicode_password_to_ansi(password).bytes
  xor_key = _create_xor_key_method1(password)
  index = password.size
  obfuscation_array = Array.new(16) { 0 }

  xor_key_high = xor_key >> 8
  xor_key_low = xor_key & 0xFF

  if (index & 1) == 1
    obfuscation_array[index] = _xor_ror(XOR_PAD[0], xor_key_high)
    index -= 1
    obfuscation_array[index] = _xor_ror(ansi_password[-1], xor_key_low)
  end

  while index > 0
    index -= 1
    obfuscation_array[index] = _xor_ror(ansi_password[index], xor_key_high)
    index -= 1
    obfuscation_array[index] = _xor_ror(ansi_password[index], xor_key_low)
  end

  index = 15
  pad_index = 15 - password.size
  while pad_index > 0
    obfuscation_array[index] = _xor_ror(XOR_PAD[pad_index], xor_key_high)
    index -= 1
    pad_index -= 1
    obfuscation_array[index] = _xor_ror(XOR_PAD[pad_index], xor_key_low)
    index -= 1
    pad_index -= 1
  end

  obfuscation_array
end

#_create_xor_key_method1(password) ⇒ Integer

2.3.7.2 Binary Document XOR Array Initialization Method 1 FUNCTION CreateXorKey_Method1

Parameters:

  • password (String)

Returns:

  • (Integer)


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/unxls/offcrypto.rb', line 45

def _create_xor_key_method1(password)
  ansi_password = _unicode_password_to_ansi(password)

  xor_key = XOR_INITIAL_CODE[ansi_password.size - 1]
  current_element = 0x68

  ansi_password.bytes.reverse.each do |c|
    7.times do
      xor_key ^= XOR_MATRIX[current_element] unless (c & 0x40).zero?
      c <<= 1
      current_element -= 1
    end
  end

  xor_key
end

#_openssl_obj(type, name) ⇒ OpenSSL::Cipher, OpenSSL::Digest

Returns e.g. OpenSSL::Cipher::RC4 instance.

Parameters:

  • type (Symbol)

    e.g. :Cipher, :Digest

  • name (Symbol)

    e.g. :RC4, SHA1

Returns:

  • (OpenSSL::Cipher, OpenSSL::Digest)

    e.g. OpenSSL::Cipher::RC4 instance



419
420
421
# File 'lib/unxls/offcrypto.rb', line 419

def _openssl_obj(type, name)
  Module.class_eval("OpenSSL::#{type}").new(name.to_s)
end

#_rc4_decrypt(data, key) ⇒ String

Parameters:

  • data (String)
  • key (String)

Returns:

  • (String)


223
224
225
226
227
228
# File 'lib/unxls/offcrypto.rb', line 223

def _rc4_decrypt(data, key)
  cipher = OpenSSL::Cipher::RC4.new
  cipher.decrypt
  cipher.key = key
  cipher.update(data) + cipher.final
end

#_rc4_make_key(password, salt, block_num) ⇒ String

2.3.6.2 Encryption Key Derivation

Parameters:

  • password (String)
  • salt (String)
  • block_num (Integer)

Returns:

  • (String)


198
199
200
201
202
203
204
# File 'lib/unxls/offcrypto.rb', line 198

def _rc4_make_key(password, salt, block_num)
  h0 = OpenSSL::Digest::MD5.digest(password.encode(Encoding::UTF_16LE))
  buffer = (h0[0..4] + salt) * 16
  h1 = OpenSSL::Digest::MD5.digest(buffer)
  hfin = "#{h1[0..4]}#{[block_num].pack('V')}"
  OpenSSL::Digest::MD5.digest(hfin)
end

#_rc4_password_match?(key, encr_verifier, encr_verifier_hash) ⇒ Boolean

2.3.6.4 Password Verification

Parameters:

  • key (String)
  • encr_verifier (String)
  • encr_verifier_hash (String)

Returns:

  • (Boolean)


210
211
212
213
214
215
216
217
218
# File 'lib/unxls/offcrypto.rb', line 210

def _rc4_password_match?(key, encr_verifier, encr_verifier_hash)
  cipher = OpenSSL::Cipher::RC4.new
  cipher.decrypt
  cipher.key = key
  decrypted_verifier = cipher.update(encr_verifier) + cipher.final
  decrypted_verifier_hash = cipher.update(encr_verifier_hash) + cipher.final
  hashed_verifier = OpenSSL::Digest::MD5.digest(decrypted_verifier)
  hashed_verifier == decrypted_verifier_hash
end

#_rc4cryptoapi_decrypt(data, key, encr_alg) ⇒ String

Parameters:

  • data (String)
  • key (String)
  • encr_alg (Symbol)

Returns:

  • (String)


409
410
411
412
413
414
# File 'lib/unxls/offcrypto.rb', line 409

def _rc4cryptoapi_decrypt(data, key, encr_alg)
  cipher = _openssl_obj(:Cipher, encr_alg)
  cipher.decrypt
  cipher.key = key
  cipher.update(data) + cipher.final
end

#_rc4cryptoapi_encryption_alg(encryption_header) ⇒ Symbol

See AlgID description, p.33

Parameters:

  • encryption_header (Hash)

Returns:

  • (Symbol)


338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/unxls/offcrypto.rb', line 338

def _rc4cryptoapi_encryption_alg(encryption_header)
  combination = [
    encryption_header[:Flags][:fCryptoAPI],
    encryption_header[:Flags][:fAES],
    encryption_header[:Flags][:fExternal],
    encryption_header[:AlgID]
  ]

  case combination
  when [false, false, true,  0x0000] then :'Determined by the application'

  when [true,  false, false, 0x0000],
       [true,  false, false, 0x6801] then :RC4

  when [true,  true,  false, 0x0000],
       [true,  true,  false, 0x660E] then :'AES-128-CBC'

  when [true,  true,  false, 0x660F] then :'AES-192-CBC'

  when [true,  true,  false, 0x6610] then :'AES-256-CBC'

  else raise "Unknown Flags and AlgID combination: #{combination}"
  end
end

#_rc4cryptoapi_hashing_alg(encryption_header) ⇒ Symbol

See AlgIDHash description, p.33

Parameters:

  • encryption_header (Hash)

Returns:

  • (Symbol)


319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/unxls/offcrypto.rb', line 319

def _rc4cryptoapi_hashing_alg(encryption_header)
  combination = [
    encryption_header[:AlgIDHash],
    encryption_header[:Flags][:fExternal]
  ]

  case combination
  when [0x0000, true] then :'Determined by the application'

  when [0x0000, false],
       [0x8004, false] then :SHA1

  else raise "Unknown Flags and AlgIDHash combination: #{combination}"
  end
end

#_rc4cryptoapi_make_key(password, salt, block_num, key_size, hash_alg) ⇒ String

2.3.5.2 RC4 CryptoAPI Encryption Key Generation

Parameters:

  • password (String)
  • salt (String)
  • block_num (Integer)
  • key_size (Integer)
  • hash_alg (Symbol)

Returns:

  • (String)


370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/unxls/offcrypto.rb', line 370

def _rc4cryptoapi_make_key(password, salt, block_num, key_size, hash_alg)
  h0_digest = _openssl_obj(:Digest, hash_alg)
  h0_digest.update(salt)
  h0_digest.update(password.encode(Encoding::UTF_16LE))
  h0 = h0_digest.digest

  h_digest = _openssl_obj(:Digest, hash_alg)
  h_digest.update(h0)
  h_digest.update([block_num].pack('V'))
  h = h_digest.digest

  h_fin = h[0..(key_size - 1)]
  h_fin << ("\x00" * (16 - key_size)) if key_size < 16

  h_fin
end

#_rc4cryptoapi_password_match?(key, encr_verifier, encr_verifier_hash, verifier_hash_size, hash_alg, encr_alg) ⇒ Boolean

2.3.5.6 Password Verification

Parameters:

  • key (String)
  • encr_verifier (String)
  • encr_verifier_hash (String)
  • verifier_hash_size (Integer)
  • hash_alg (Symbol)
  • encr_alg (Symbol)

Returns:

  • (Boolean)


394
395
396
397
398
399
400
401
402
403
# File 'lib/unxls/offcrypto.rb', line 394

def _rc4cryptoapi_password_match?(key, encr_verifier, encr_verifier_hash, verifier_hash_size, hash_alg, encr_alg)
  cipher = _openssl_obj(:Cipher, encr_alg)
  cipher.decrypt
  cipher.key = key
  decrypted_verifier = cipher.update(encr_verifier) + cipher.final
  decrypted_verifier_hash = cipher.update(encr_verifier_hash) + cipher.final
  decrypted_verifier_hash = decrypted_verifier_hash[0..(verifier_hash_size - 1)]
  hashed_verifier = _openssl_obj(:Digest, hash_alg).digest(decrypted_verifier)
  hashed_verifier == decrypted_verifier_hash
end

#_unicode_password_to_ansi(password) ⇒ String

Note:

This method works reliably only for passwords consisting of ASCII characters

See 2.3.7.4 Binary Document Password Verifier Derivation Method 2 (p. 61)

Parameters:

  • password (String)

Returns:

  • (String)


130
131
132
133
134
135
# File 'lib/unxls/offcrypto.rb', line 130

def _unicode_password_to_ansi(password)
  password.encode(Encoding::UTF_16LE).unpack('v*').map do |c|
    low_byte = c & 0xFF
    low_byte.zero? ? ((c >> 8) & 0xFF) : low_byte
  end.pack('C*')
end

#_xor_decrypt_byte(byte, byte_index, xor_decryption_array, record_offset, record_size) ⇒ String

Parameters:

  • byte (Integer)
  • byte_index (Integer)
  • xor_decryption_array (Array<Integer>)
  • record_offset (Integer)
  • record_size (Integer)

Returns:

  • (String)


149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/unxls/offcrypto.rb', line 149

def _xor_decrypt_byte(byte, byte_index, xor_decryption_array, record_offset, record_size)
  # "The initial value for XorArrayIndex is as follows:
  # XorArrayIndex = (FileOffset + Data.Length) % 16
  # The FileOffset variable in this context is the stream offset into the Workbook stream at
  # the time we are about to write each of the bytes of the record data.
  # This (the value) is then incremented after each byte is written."
  # From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5
  xor_array_index = (record_offset + Unxls::Biff8::Record::HEADER_SIZE + record_size + byte_index) & 0xF

  byte ^= xor_decryption_array[xor_array_index]
  byte = ((byte << 3) | (byte >> 5)) & 0xFF # rotate right 5 bits

  [byte].pack('C')
end

#_xor_password_match?(password, verification_bytes) ⇒ Boolean

Parameters:

  • password (String)
  • verification_bytes (String)

Returns:

  • (Boolean)


122
123
124
# File 'lib/unxls/offcrypto.rb', line 122

def _xor_password_match?(password, verification_bytes)
  _xor_password_verifier_method1(password) == verification_bytes
end

#_xor_password_verifier_method1(password) ⇒ Integer

2.3.7.1 Binary Document Password Verifier Derivation Method 1 FUNCTION CreatePasswordVerifier_Method1

Parameters:

  • password (String)

Returns:

  • (Integer)


106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/unxls/offcrypto.rb', line 106

def _xor_password_verifier_method1(password)
  verifier = 0
  ansi_password = _unicode_password_to_ansi(password)

  ansi_password.bytes.unshift(ansi_password.size).reverse.each do |b|
    int1 = (verifier & 0b0100_0000_0000_0000).zero? ? 0 : 1
    int2 = (verifier << 1) & 0b0111_1111_1111_1111
    int3 = int1 | int2
    verifier = int3 ^ b
  end

  verifier ^ 0xCE4B
end

#_xor_ror(byte1, byte2) ⇒ Object



137
138
139
# File 'lib/unxls/offcrypto.rb', line 137

def _xor_ror(byte1, byte2)
  Unxls::BitOps.new(byte1 ^ byte2).ror(8, 1)
end

#encryptionheader(io) ⇒ Hash

2.3.2 EncryptionHeader 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.

Parameters:

  • io (StringIO)

Returns:

  • (Hash)


257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/unxls/offcrypto.rb', line 257

def encryptionheader(io)
  result = {
    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.
    SizeExtra: io.read(4).unpack('V').first, # SizeExtra (4 bytes): A field that is reserved and for which the value MUST be 0x00000000.
  }

  alg_id = io.read(4).unpack('l<').first # AlgID (4 bytes): A signed integer that specifies the encryption algorithm.
  result[:AlgID] = alg_id
  result[:AlgID_d] = {
    0x0000 => :'Determined by Flags',
    0x6801 => :RC4,
    0x660E => :AES128,
    0x660F => :AES192,
    0x6610 => :AES256
  }[alg_id]

  alg_id_hash, key_size, provider_type, _, _ = io.read(20).unpack('l<VV')
  result[:AlgIDHash] = alg_id_hash # AlgIDHash (4 bytes): A signed integer that specifies the hashing algorithm together with the Flags.fExternal bit.
  result[:KeySize] = key_size # KeySize (4 bytes): An unsigned integer that specifies the number of bits in the encryption key.
  result[:ProviderType] = provider_type # ProviderType (4 bytes): An implementation-specific value that corresponds to constants accepted by the specified CSP.
  # Reserved1 (4 bytes): A value that is undefined and MUST be ignored.
  # Reserved2 (4 bytes): A value that MUST be 0x00000000 and MUST be ignored.

  result[:CSPName] = Unxls::Oshared._db_zero_terminated(io) # CSPName (variable): A null-terminated Unicode string that specifies the CSP name.

  result
end

#encryptionheaderflags(value) ⇒ Hash

2.3.1 EncryptionHeaderFlags The EncryptionHeaderFlags structure specifies properties of the encryption algorithm used.

Parameters:

  • value (Integer)

Returns:

  • (Hash)


289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/unxls/offcrypto.rb', line 289

def encryptionheaderflags(value)
  attrs = Unxls::BitOps.new(value)

  {
    # A – Reserved1 (1 bit): A value that MUST be 0 and MUST be ignored.
    # B – Reserved2 (1 bit): A value that MUST be 0 and MUST be ignored.
    fCryptoAPI: attrs.set_at?(2), # C – fCryptoAPI (1 bit): A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption [ECMA- 376] is used.
    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.
    fExternal: attrs.set_at?(4), # E – fExternal (1 bit): A value that MUST be 1 if extensible encryption is used.
    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]
    # Unused (26 bits): A value that is undefined and MUST be ignored.
  }
end

#encryptionverifier(io) ⇒ Hash

2.3.3 EncryptionVerifier

Parameters:

  • io (StringIO)

Returns:

  • (Hash)


306
307
308
309
310
311
312
313
314
# File 'lib/unxls/offcrypto.rb', line 306

def encryptionverifier(io)
  {
    SaltSize: io.read(4).unpack('V').first, # SaltSize (4 bytes): An unsigned integer that specifies the size of the Salt field.
    Salt: io.read(16), # Salt (16 bytes): An array of bytes that specifies the salt value used during password hash generation.
    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.
    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.
    EncryptedVerifierHash: io.read # EncryptedVerifierHash (variable): An array of bytes that contains the encrypted form of the hash of the randomly generated Verifier value.
  }
end

#rc4cryptoapiheader(io) ⇒ Hash

2.3.5.1 RC4 CryptoAPI Encryption Header

Parameters:

  • io (StringIO)

Returns:

  • (Hash)


237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/unxls/offcrypto.rb', line 237

def rc4cryptoapiheader(io)
  result = {
    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.
    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.
  }

  encryption_header_size = io.read(4).unpack('V').first
  result[:EncryptionHeaderSize] = encryption_header_size # EncryptionHeaderSize (4 bytes): An unsigned integer that specifies the size, in bytes, of the EncryptionHeader structure.
  result[:EncryptionHeader] = encryptionheader(io) # EncryptionHeader (variable): An EncryptionHeader structure (section 2.3.2) used to encrypt the structure.
  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.
  result[:_encryption_algorithm] = _rc4cryptoapi_encryption_alg(result[:EncryptionHeader])
  result[:_hashing_algorithm] = _rc4cryptoapi_hashing_alg(result[:EncryptionHeader])

  result
end

#rc4encryptionheader(io) ⇒ Hash

2.3.6.1 RC4 Encryption Header

Parameters:

  • io (StringIO)

Returns:

  • (Hash)


184
185
186
187
188
189
190
191
# File 'lib/unxls/offcrypto.rb', line 184

def rc4encryptionheader(io)
  {
    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.
    Salt: io.read(16), # Salt (16 bytes): A randomly generated array of bytes that specifies the salt value used during password hash generation.
    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.
    EncryptedVerifierHash: io.read(16) # EncryptedVerifierHash (16 bytes): A 40-bit RC4 encrypted MD5 hash of the verifier used to generate the EncryptedVerifier field.
  }
end

#version(data) ⇒ Hash

2.1.4 Version The Version structure specifies the version of a product or feature. It contains a major and a minor version number.

Parameters:

  • data (String)

Returns:

  • (Hash)


172
173
174
175
176
177
178
179
# File 'lib/unxls/offcrypto.rb', line 172

def version(data)
  v_major, v_minor = data.unpack('vv')

  {
    vMajor: v_major, # vMajor (2 bytes): An unsigned integer that specifies the major version number.
    vMinor: v_minor # vMinor (2 bytes): An unsigned integer that specifies the minor version number.
  }
end