summaryrefslogtreecommitdiffhomepage
path: root/lib/axlsx/util/ms_off_crypto.rb
blob: e91bb20f2d959077fbe0462559cf574d950ae039 (plain)
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