1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
# encoding: UTF-8
require 'digest'
require 'base64'
require 'openssl'
module Axlsx
# The MsOffCrypto class implements ECMA-367 encryption based on the MS-OFF-CRYPTO specification
class MsOffCrypto
# Creates a new MsOffCrypto Object
# @param [String] file_name the location of the file you want to encrypt
# @param [String] pwd the password to use when encrypting the file
def initialize(file_name, pwd)
self.password = pwd
self.file_name = file_name
end
# Generates a new CBF file based on this instance of ms-off-crypto and overwrites the unencrypted file.
def save
cfb = Cbf.new(self)
cfb.save
end
# returns the raw password used in encryption
# @return [String]
attr_reader :password
# sets the password to be used for encryption
# @param [String] v the password, @default 'password'
# @return [String]
def password=(v)
@password = v || 'password'
end
# retruns the file name of the archive to be encrypted
# @return [String]
attr_reader :file_name
# sets the filename
# @return [String]
def file_name=(v)
#TODO verfify that the file specified exists and is an unencrypted xlsx archive
@file_name = v
end
# encrypts and returns the package specified by the file name
# @return [String]
def encrypted_package
@encrypted_package ||= encrypt_package(file_name)
end
# returns the encryption info for this instance of ms-off-crypto
# @return [String]
def encryption_info
@encryption_info ||= create_encryption_info
end
# returns a random salt
# @return [String]
def salt
@salt ||= Digest::SHA1.digest(rand(16**16).to_s)
end
# returns a random verifier
# @return [String]
def verifier
@verifier ||= rand(16**16).to_s
end
# returns the verifier encrytped
# @return [String]
def encrypted_verifier
@encrypted_verifier ||= encrypt(verifier)
end
# returns the verifier hash encrypted
# @return [String]
def encrypted_verifier_hash
@encrypted_verifier_hash ||= encrypt(verifier_hash)
end
# returns a verifier hash
# @return [String]
def verifier_hash
@verifier_hash ||= create_verifier_hash
end
# returns an encryption key
# @return [String]
def key
@key ||= create_key
end
# size of unencrypted package? concated with encrypted package
def encrypt_package(file_name)
package = File.open(file_name, 'r')
crypt_pack = encrypt(package.read)
[crypt_pack.size].pack('q') + crypt_pack
end
# Generates an encryption info structure
# @return [String]
def create_encryption_info
header = [3, 0, 2, 0] # version
# Header flags copy
header.concat [0x24, 0, 0, 0] #flags -- VERY UNSURE ABOUT THIS STILL
# header.concat [0, 0, 0, 0] #unused
header.concat [0xA4, 0, 0, 0] #length
# Header
header.concat [0x24, 0, 0, 0] #flags again
# header.concat [0, 0, 0, 0] #unused again,
header.concat [0x0E, 0x66, 0, 0] #alg id
header.concat [0x04, 0x80, 0, 0] #alg hash id
header.concat [key.size, 0, 0, 0] #key size
header.concat [0x18, 0, 0, 0] #provider type
# header.concat [0, 0, 0, 0] #reserved 1
# header.concat [0, 0, 0, 0] #reserved 2
header.concat [0xA0, 0xC7, 0xDC, 0x2, 0, 0, 0, 0]
header.concat "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)".bytes.to_a.pack('s*').bytes.to_a
header.concat [0, 0] #null terminator
#Salt Size
header.concat [salt.bytes.to_a.size].pack('l').bytes.to_a
#Salt
header.concat salt.bytes.to_a.pack('c*').bytes.to_a
# encryption verifier
header.concat encrypted_verifier.bytes.to_a.pack('c*').bytes.to_a
# verifier hash size -- MUST BE 32 bytes
header.concat [verifier_hash.bytes.to_a.size].pack('l').bytes.to_a
#encryption verifier hash
header.concat encrypted_verifier_hash.bytes.to_a.pack('c*').bytes.to_a
header.flatten!
header.pack('c*')
end
# 2.3.3
def create_verifier_hash
vh = Digest::SHA1.digest(verifier)
vh << Array.new(32 - vh.size, 0).join('')
end
# 2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)
def create_key
sha = Digest::SHA1.new() << (salt + @password)
(0..49999).each { |i| sha.update(i.to_s+sha.to_s) }
key = sha.update(sha.to_s+'0').digest
a = key.bytes.each_with_index.map { |item, i| 0x36 ^ item }
x1 = Digest::SHA1.digest((a.concat Array.new(64 - key.size, 0x36)).to_s)
a = key.bytes.each_with_index.map { |item, i| 0x5C ^ item }
x2 = Digest::SHA1.digest( (a.concat Array.new(64 - key.size, 0x5C) ).to_s)
x3 = x1 + x2
x3.bytes.to_a[(0..31)].pack('c*')
end
# ensures that the a hashed decryption of the encryption verifier matches the decrypted verifier hash.
# @return [Boolean]
def verify_password
v = Digest::SHA1.digest decrypt(@encrypted_verifier)
vh = decrypt(@encrypted_verifier_hash)
vh[0..15] == v[0..15]
end
# encrypts the data proved
# @param [String] data
# @return [String] the encrypted data
def encrypt(data)
aes = OpenSSL::Cipher.new("AES-128-ECB")
aes.encrypt
aes.key = key
aes.update(data) << aes.final
end
# dencrypts the data proved
# @param [String] data
# @return [String] the dencrypted data
def decrypt(data)
aes = OpenSSL::Cipher.new("AES-128-ECB")
aes.decrypt
aes.key = key
aes.update(data) << aes.final
end
end
end
|