summaryrefslogtreecommitdiffhomepage
path: root/lib/axlsx/util/cbf.rb
blob: f8fc057b63e92bc0decb6ea9fe6f99c94015611a (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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# encoding: UTF-8
module Axlsx

  # The Cfb class is a MS-OFF-CRYPTOGRAPHY specific OLE (MS-CBF) writer implementation. No attempt is made to re-invent the wheel for read/write of compound binary files.
  class Cbf

    # the serialization for the CBF FAT
    FAT_PACKING  = "s128"

    # the serialization for the MS-OFF-CRYPTO version stream
    VERSION_PACKING = 'l s30 l3'

    # The serialization for the MS-OFF-CRYPTO dataspace map stream
    DATA_SPACE_MAP_PACKING = 'l6 s16 l s25 x2'

    # The serialization for the MS-OFF-CRYPTO strong encrytion data space stream
    STRONG_ENCRYPTION_DATA_SPACE_PACKING = 'l3 s26'

    # The serialization for the MS-OFF-CRYPTO primary stream
    PRIMARY_PACKING = 'l3 s38 l s40 l3 x12 l'

    # The cutoff size that determines if a stream should be in the mini-fat or the fat
    MINI_CUTOFF = 4096

    # The serialization for CBF header
    HEADER_PACKING = "q x16 l s3 x10 l l x4 l*"

    # Creates a new Cbf object based on the ms_off_crypto object provided.
    # @param [MsOffCrypto] ms_off_crypto
    def initialize(ms_off_crypto)
      @file_name = ms_off_crypto.file_name  
      @ms_off_crypto = ms_off_crypto
      create_storages
      mini_fat_stream
      mini_fat
      fat
      header
    end

    # creates or returns the version storage
    # @return [Storage]
    def version
      @version ||= create_version
    end

    # returns the data space map storage
    # @return [Storage]
    def data_space_map
      @data_space_map ||= create_data_space_map
    end

    # returns the primary storage
    # @return [Storgae]
    def primary
      @primary ||= create_primary
    end

    # returns the summary information storage
    # @return [Storage]
    def summary_information
      @summary_information ||= create_summary_information
    end

    # returns the document summary information 
    # @return [Storage]
    def document_summary_information
      @document_summary_information ||= create_document_summary_information
    end

    # returns the stream of data allocated in the fat
    # @return [String]
    def fat_stream 
      @fat_stream ||= create_fat_stream
    end

    # returns the stream allocated in the mini fat.
    # return [String]
    def mini_fat_stream
      @mini_fat_stream ||= create_mini_fat_stream
    end
    
    # returns the mini fat
    # return [String]
    def mini_fat
      @mini_fat ||= create_mini_fat
    end

    # returns the fat
    # @return [String]
    def fat
      @fat ||= create_fat
    end
    
    # returns the CFB header
    # @return [String]
    def header
      @header ||= create_header
    end

    # returns the encryption info from the ms_off_crypt object provided during intialization
    # @return [String] encryption info
    def encryption_info
      @ms_off_crypto.encryption_info
    end

    # returns the encrypted package from the ms_off_crypt object provided during initalization
    # @return [String] encrypted package
    def encrypted_package
      @ms_off_crypto.encrypted_package
    end

    # writes the compound binary file to disk
    def save
      ole = File.open(@file_name, 'w')
      ole << header
      ole << fat
      @storages.each { |s| ole << s.to_s }
      ole << Array.new((512-(ole.pos % 512)), 0).pack('c*')
      ole << mini_fat
      ole << mini_fat_stream
      ole << fat_stream
      ole.close
    end
    
    private

    # Generates the storages required for ms-office-cryptography cfb
    def create_storages
      @storages = []
      @encryption_info = @ms_off_crypto.encryption_info
      @encrypted_package = @ms_off_crypto.encrypted_package

      @storages << Storage.new('EncryptionInfo', :data=>encryption_info, :left=>3, :right=>11) # example shows right child. do we need the summary info????
      @storages << Storage.new('EncryptedPackage', :data=>encrypted_package, :color=>Storage::COLORS[:red])
      @storages << Storage.new([6].pack("c")+"DataSpaces", :child=>5, :modified =>129685612740945580, :created=>129685612740819979)
      @storages << version
      @storages << data_space_map
      @storages << Storage.new('DataSpaceInfo', :right=>8, :child=>7, :created=>129685612740828880,:modified=>129685612740831800)
      @storages << strong_encryption_data_space
      @storages << Storage.new('TransformInfo', :color => Storage::COLORS[:red],  :child=>9, :created=>129685612740834130, :modified=>129685612740943959)
      @storages << Storage.new('StrongEncryptionTransform', :child=>10, :created=>129685612740834169, :modified=>129685612740942280)
      @storages << primary      
      # @storages << summary_information
      # @storages << document_summary_information

      # we do this at the end as we need to build the minifat stream to determine the size. #HOWEVER - it looks like the size should not include the padding?
      @storages.unshift Storage.new('Root Entry', :type=>Storage::TYPES[:root], :color=>Storage::COLORS[:red], :child=>1, :data => mini_fat_stream)

    end

    # generates the mini fat stream
    # @return [String]
    def create_mini_fat_stream
      mfs = []
      @storages.select{ |s| s.type == Storage::TYPES[:stream] && s.size < MINI_CUTOFF}.each_with_index do |stream, index|
        puts "#{stream.name.pack('c*')}: #{stream.data.size}"        
        mfs.concat stream.data
        mfs.concat Array.new(64 - (mfs.size % 64), 0) if mfs.size % 64 > 0        
        puts "mini fat stream size: #{mfs.size}"
      end
      mfs.concat(Array.new(512 - (mfs.size % 512), 0))
      mfs.pack 'c*'
    end

    # generates the fat stream.
    # @return [String]
    def create_fat_stream
      mfs = []
      @storages.select{ |s| s.type == Storage::TYPES[:stream] && s.size >= MINI_CUTOFF}.each_with_index do |stream, index|
        mfs.concat stream.data
        mfs.concat Array.new(512 - (mfs.size % 512), 0) if mfs.size % 512 > 0     
      end
      mfs.pack 'c*'
    end

    # creates the mini fat
    # @return [String]
    def create_mini_fat
      v_mf = []
      @storages.select{ |s| s.type == Storage::TYPES[:stream] && s.size < MINI_CUTOFF}.each do |stream|
        allocate_stream(v_mf, stream, 64)
      end
      v_mf.concat Array.new(128 - v_mf.size, -1)
      v_mf.pack 'l*'
    end

    # creates the fat
    # @return [String]
    def create_fat
      v_fat = [-3]
      # storages four per sector, allocation forces directories to start at sector ID 0
      allocate_stream(v_fat, @storages, 4)
      # fat entry for minifat
      allocate_stream(v_fat, 0, 512)
      # fat entry for minifat stream
      @storages[0].sector = v_fat.size
      allocate_stream(v_fat, mini_fat_stream, 512)
      # fat entries for encrypted package storage
      # what to do about DIFAT for larger packages...
      if @encrypted_package.size > (109 - v_fat.size) * 512
        raise ArgumentError, "Your package is too big!"
      end

      if @encrypted_package.size >= MINI_CUTOFF
        allocate_stream(v_fat, @encrypted_package, 512)
      end

      v_fat.concat Array.new(128 - v_fat.size, -1) if v_fat.size < 128 #pack in unused sectors
      v_fat.pack 'l*'
    end
    
    # Creates the version storage
    # @return [Storage]
    def create_version
      v_stream= [60, "Microsoft.Container.DataSpaces".bytes.to_a, 1, 1, 1].flatten!.pack VERSION_PACKING
      Storage.new('Version', :data=>v_stream, :size=>v_stream.size)
    end

    # returns the strong encryption data space storage
    # @return [Storgae]
    def strong_encryption_data_space
      @strong_encryption_data_space ||= create_strong_encryption_data_space
    end

    # Creates the data space map storage
    # @return [Storgae]
    def create_data_space_map
      v_stream = [8,1,104, 1,0, 32, "EncryptedPackage".bytes.to_a, 50, "StrongEncryptionDataSpace".bytes.to_a].flatten!.pack DATA_SPACE_MAP_PACKING      
      Storage.new('DataSpaceMap', :data=>v_stream, :left => 4, :right => 6, :size=>v_stream.size)
    end


    # creates the stron encryption data space storage
    # @return [Storgae]
    def create_strong_encryption_data_space
      v_stream = [8,1,50,"StrongEncryptionTransform".bytes.to_a,0].flatten.pack STRONG_ENCRYPTION_DATA_SPACE_PACKING
      Storage.new("StrongEncryptionDataSpace", :data=>v_stream, :size => v_stream.size)
    end
    
    # creates the primary storage
    # @return [Storgae]
    def create_primary
      v_stream = [88,1,76,"{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}".bytes.to_a].flatten
      v_stream.concat [78, "Microsoft.Container.EncryptionTransform".bytes.to_a,0,1,1,1,4].flatten
      v_stream = v_stream.pack PRIMARY_PACKING
      Storage.new([6].pack("c")+"Primary", :data=>v_stream)
    end


    # creates the summary information storage
    # @return [Storage]
    def create_summary_information
      v_stream = []
      v_stream.concat [0xFEFF, 0x0000, 0x030A, 0x0100, 0x0000, 0x0000, 0x0000, 0x0000]
      v_stream.concat [0x0000, 0x0000, 0x0000, 0x0000, 0x0100, 0x0000, 0xE085, 0x9FF2]
      v_stream.concat [0xF94F, 0x6810, 0xAB91, 0x0800, 0x2B27, 0xB3D9, 0x3000, 0x0000]
      v_stream.concat [0xAC00, 0x0000, 0x0700, 0x0000, 0x0100, 0x0000, 0x4000, 0x0000]
      v_stream.concat [0x0400, 0x0000, 0x4800, 0x0000, 0x0800, 0x0000, 0x5800, 0x0000]
      v_stream.concat [0x1200, 0x0000, 0x6800, 0x0000, 0x0C00, 0x0000, 0x8C00, 0x0000]
      v_stream.concat [0x0D00, 0x0000, 0x9800, 0x0000, 0x1300, 0x0000, 0xA400, 0x0000]
      v_stream.concat [0x0200, 0x0000, 0xE9FD, 0x0000, 0x1E00, 0x0000, 0x0800, 0x0000]
      v_stream.concat [0x7261, 0x6E64, 0x796D, 0x0000, 0x1E00, 0x0000, 0x0800, 0x0000]
      v_stream.concat [0x7261, 0x6E64, 0x796D, 0x0000, 0x1E00, 0x0000, 0x1C00, 0x0000]
      v_stream.concat [0x4D69, 0x6372, 0x6F73, 0x6F66, 0x7420, 0x4D61, 0x6369, 0x6E74]
      v_stream.concat [0x6F73, 0x6820, 0x4578, 0x6365, 0x6C00, 0x0000, 0x4000, 0x0000]
      v_stream.concat [0x10AC, 0x5396, 0x60BC, 0xCC01, 0x4000, 0x0000, 0x40F4, 0xFDAF]
      v_stream.concat [0x60BC, 0xCC01, 0x0300, 0x0000, 0x0100, 0x0000]

      v_stream = v_stream.pack "s*"

      Storage.new([5].pack('c')+"SummaryInformation", :data=>v_stream, :left => 2)
    end


    # creates the document summary information storage
    # @return [Storage]
    def create_document_summary_information
      v_stream = []
      v_stream.concat [0xFEFF, 0x0000, 0x030A, 0x0100, 0x0000, 0x0000, 0x0000, 0x0000]
      v_stream.concat [0x0000, 0x0000, 0x0000, 0x0000, 0x0100, 0x0000, 0x02D5, 0xCDD5]
      v_stream.concat [0x9C2E, 0x1B10, 0x9397, 0x0800, 0x2B2C, 0xF9AE, 0x3000, 0x0000]
      v_stream.concat [0xCC00, 0x0000, 0x0900, 0x0000, 0x0100, 0x0000, 0x5000, 0x0000]
      v_stream.concat [0x0F00, 0x0000, 0x5800, 0x0000, 0x1700, 0x0000, 0x6400, 0x0000]
      v_stream.concat [0x0B00, 0x0000, 0x6C00, 0x0000, 0x1000, 0x0000, 0x7400, 0x0000]
      v_stream.concat [0x1300, 0x0000, 0x7C00, 0x0000, 0x1600, 0x0000, 0x8400, 0x0000]
      v_stream.concat [0x0D00, 0x0000, 0x8C00, 0x0000, 0x0C00, 0x0000, 0x9F00, 0x0000]
      v_stream.concat [0x0200, 0x0000, 0xE9FD, 0x0000, 0x1E00, 0x0000, 0x0400, 0x0000]
      v_stream.concat [0x0000, 0x0000, 0x0300, 0x0000, 0x0000, 0x0C00, 0x0B00, 0x0000]
      v_stream.concat [0x0000, 0x0000, 0x0B00, 0x0000, 0x0000, 0x0000, 0x0B00, 0x0000]
      v_stream.concat [0x0000, 0x0000, 0x0B00, 0x0000, 0x0000, 0x0000, 0x1E10, 0x0000]
      v_stream.concat [0x0100, 0x0000, 0x0700, 0x0000, 0x5368, 0x6565, 0x7431, 0x000C]
      v_stream.concat [0x1000, 0x0002, 0x0000, 0x001E, 0x0000, 0x0013, 0x0000, 0x00E3]
      v_stream.concat [0x83AF, 0xE383, 0xBCE3, 0x82AF, 0xE382, 0xB7E3, 0x83BC, 0xE383]
      v_stream.concat [0x8800, 0x0300, 0x0000, 0x0100, 0x0000, 0x0000]
      v_stream = v_stream.pack 'c*'
      Storage.new([5].pack('c')+"DocumentSummaryInformation", :data=>v_stream)
    end

    # Creates the header 
    # @return [String]
    def create_header
      header = []
      header << -2226271756974174256 # identifier pack as q
      header << 196670 # version pack as L
      header << 65534 # byte order pack as s
      header << 9 # sector shift
      header << 6 # mini-sector shift
      header << (fat.size/512.0).ceil # this is the number of FAT sectors in the file at index 6 pack as L
      header << header.last # this is the first directory sector, index of 7 pack as L
      header << MINI_CUTOFF # minfat cutoff pack as L
      # MiniFat starts after directories
      header << (fat.size/512.0).ceil + (@storages.size/4.0).ceil # this is the sector id for the first minifat index 10 pack as L
      header << (mini_fat.size/512.0).ceil # minifat sector count index 11 pack as L
      header << -2 # the first DIFAT - set to end of chain until we exceed a single FAT pack as L
      header << 0 # number of DIFAT sectors, unless we go beyond 109 FAT sectors this will always be 0 pack as L
      header << 0 # first FAT sector defined in the DIFAT pack as L
      header.concat Array.new(108, -1) # Difat sectors pack as L108
      header.pack(HEADER_PACKING)
    end

    # Allocates sector chains in a allocation table based on the sector size and stream provided
    # If a storage obeject is provided, the starting sector value for the storage is updated based on the allocation performed here.
    # @param [Array] table Allocation table array
    # @param [Storage | String] stream
    # @param [Integer] size The cutoff size for the stream. 
    def allocate_stream(table, stream, size)
      stream.sector = table.size if stream.respond_to?(:sector)
      ((stream.size / size.to_f).ceil).times { table << table.size }
      table[table.size-1] = -2 # this is the CBF chain terminator
    end

  end
end