zipography 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1827f4d1fd43192ac37e029981ab04de876a87d8a9e91b044873d671de1738cc
4
- data.tar.gz: c2de37c58542aca50915c71712cda9f420cc2dded28cf95a9af1c369465774f7
3
+ metadata.gz: 1d865aef5a55e1a953eb86bbbdb97d2e1f78a9c9eb77e81b7954cf37f9d154d6
4
+ data.tar.gz: 5616046d45ccc02435baa0c99e7b1fc799faca97e2daa2762c5f8b98fe17ccd3
5
5
  SHA512:
6
- metadata.gz: 960794c6d6a3cdb205c54e4b9ef1c38d840ca061e6fd547340c1122c88310312a19f2ff40e481a01d365bc8e76076fe01ee6d8a1fe25e243ce7e086c271fd24a
7
- data.tar.gz: 381072f98336e90d7dc953821a7c5812cf736b09a0f19216915acdd5c82a498e7fb1f4e4c911c2519f8222484a26305fbd1becac4ee9fa15907adc61c2a00b66
6
+ metadata.gz: 89a240e96e8f953a1a9072e53b90ba2a4a27896170d3382ad0fbfa8e5e532360feee56184ac047c3e09ad77f36b7742c167b9100e3224eaa7c5de8a14e6942eb
7
+ data.tar.gz: cbf032cfcfa94752a5cac53f4f0ad5bf2de13e3a842b6a001df36fc8e177ce4706893707e8ee83979e58bcaa9d13d1b3cf376b9eab8a9bc4930aeb9578e2b582
data/README.md CHANGED
@@ -9,11 +9,17 @@ file managers (Windows Explorer), the blob is invisible.
9
9
  Limitations:
10
10
 
11
11
  * single blob only (but you can just add another .zip as a blob);
12
- * doesn't work w/ zip64 files (this means 4GB max for an archive+blob
13
- combo);
14
- * corrupts zip spec v6.2+ files that use *central directory*
15
- encryption;
16
- * memory hungry, for it's just a toy, not a serious spy tool.
12
+ * doesn't work w/ zip64 files (this means ~4GB max for an archive+blob
13
+ combo).
14
+
15
+ ## How does it work?
16
+
17
+ A blob is injected after a file section right before the 1st *central
18
+ directory header*. After that, a pointer in an *end of central
19
+ directory* record is updated to compensate the shift of the *central
20
+ directory header*.
21
+
22
+ <img src='doc/zip.svg'>
17
23
 
18
24
  ## Usage
19
25
 
@@ -32,7 +38,9 @@ Inject a picture into the archive:
32
38
 
33
39
  $ zipography-inject orig.zip blob1.png > 1.zip
34
40
 
35
- It it visible? No:
41
+ (On Windows, use `-o 1.zip`, instead of a redirection.)
42
+
43
+ Is it visible? It isn't:
36
44
 
37
45
  ~~~
38
46
  $ bsdtar tf 1.zip
@@ -51,7 +59,7 @@ Check if we have the picture in the archive:
51
59
  $ zipography-info 1.zip
52
60
  Payload size: 18313
53
61
  Adler32: 0x6812d9f
54
- Version: 1
62
+ Blob version: 1
55
63
  Valid: true
56
64
  ~~~
57
65
 
@@ -64,15 +72,6 @@ $ xdg-open !$
64
72
 
65
73
  <img src='test/blob1.png'>
66
74
 
67
- ## How does it work?
68
-
69
- A blob is injected after a file section right before the 1st *central
70
- directory header*. After that, a pointer in an *end of central
71
- directory* record is updated to compensate the shift of the *central
72
- directory header*.
73
-
74
- <img src='doc/zip.svg'>
75
-
76
75
  ## License
77
76
 
78
77
  MIT.
data/lib.rb CHANGED
@@ -1,80 +1,186 @@
1
+ require 'optparse'
1
2
  require 'zlib'
2
3
  require 'bindata'
3
4
 
4
5
  module Zipography
5
-
6
- HEADER_SIZE = 9 # bytes
7
- class HiddenBlob < BinData::Record
6
+ class Eocd < BinData::Record
8
7
  endian :little
9
8
 
10
- uint32 :checksum
11
- uint32 :len
12
- uint8 :version, initial_value: 1
9
+ uint32 :signature, asserted_value: 0x06054b50
10
+ uint16 :disk
11
+ uint16 :disk_cd_start
12
+ uint16 :cd_entries_disk
13
+ uint16 :cd_entries_total
14
+ uint32 :cd_size
15
+ uint32 :cd_offset_start
16
+ uint16 :comment_len
17
+ string :comment, :read_length => :comment_len,
18
+ onlyif: -> { comment_len.nonzero? }
13
19
  end
14
20
 
15
- def checksum s; Zlib.adler32(s); end
21
+ class IOReverseChunks
22
+ include Enumerable
16
23
 
17
- def blob_make file
18
- payload = File.read file
19
- header = HiddenBlob.new len: File.stat(file).size,
20
- checksum: checksum(payload)
21
- [payload.force_encoding('ASCII-8BIT'), header.to_binary_s].join
22
- end
24
+ def initialize io, bufsiz: 4096, debug: false
25
+ @io = io
26
+ @fsize = io.stat.size
27
+ @bufsiz = bufsiz > @fsize ? (@fsize/2.0).ceil : bufsiz
28
+
29
+ @seek = -@bufsiz
30
+ @io.seek @seek, IO::SEEK_END
23
31
 
24
- class MyZip
25
- def initialize file
26
- @file = file
27
- @buf = File.read file
28
- @eocd = end_of_central_dir
29
- @first_cdh = first_central_dir_header
32
+ @debug = debug
33
+ $stderr.puts "#{@io.path}: seek=#{@seek}, bufsiz=#{@bufsiz}" if @debug
30
34
  end
35
+ attr_reader :io, :fsize
36
+
37
+ def each
38
+ idx = 0
39
+ while (chunk = @io.read(@bufsiz))
40
+ return if chunk.size == 0
31
41
 
32
- def first_central_dir_header
33
- @buf.index [0x02014b50].pack('V')
42
+ yield chunk
43
+ idx += 1
44
+
45
+ return if @seek == 0
46
+
47
+ @seek = -@bufsiz*(idx+1)
48
+ begin
49
+ @io.seek @seek, IO::SEEK_END
50
+ rescue Errno::EINVAL
51
+ @bufsiz = -(@bufsiz*(idx+1) - @fsize - @bufsiz)
52
+ @seek = 0
53
+ @io.seek @seek
54
+ end
55
+
56
+ $stderr.puts "#{@io.path}: seek=#{@seek}, bufsiz=#{@bufsiz}" if @debug
57
+ end
34
58
  end
59
+ end
35
60
 
36
- def end_of_central_dir
37
- pos = @buf.rindex [0x06054b50].pack('V')
38
- fail 'not a zip file' unless pos
39
- pos
61
+ def file_rindex file, substring
62
+ my_rindex = ->(s1, s2) { s1.rindex s2.dup.force_encoding('ASCII-8BIT') }
63
+
64
+ r = nil
65
+ File.open(file, 'rb') do |io|
66
+ iorc = IOReverseChunks.new io
67
+ prev_chunk = ''
68
+ bytes_read = 0
69
+
70
+ r = iorc.each do |chunk|
71
+ bytes_read += chunk.size
72
+
73
+ if (idx = my_rindex.call(chunk, substring))
74
+ break iorc.fsize - bytes_read + idx
75
+ end
76
+
77
+ if my_rindex.call(chunk, substring[0])
78
+ two_chunks = chunk + prev_chunk
79
+ if (idx = my_rindex.call(two_chunks, substring))
80
+ break iorc.fsize - bytes_read + idx
81
+ end
82
+ end
83
+
84
+ prev_chunk = chunk
85
+ end
40
86
  end
41
87
 
42
- # start of central dir offset
43
- def offset
44
- @buf.slice(@eocd+16, 4).unpack('V').first
88
+ r
89
+ end
90
+
91
+ def eocd_position file
92
+ file_rindex(file, [0x06054b50].pack('V')) || fail("#{file}: not a zip")
93
+ end
94
+
95
+ def eocd_parse file, pos
96
+ File.open(file, 'rb') do |f|
97
+ f.seek pos
98
+ r = Eocd.read f
99
+ fail "no support for zip64 format" if r[:cd_offset_start] == 0xFFFFFFFF
100
+ r
45
101
  end
102
+ end
46
103
 
47
- # very crude: instead of an intelligent parsing of eocd, modifying
48
- # an offset, replacing eocd, we just change the offset. this won't
49
- # fly for zip64 files
50
- def repack data
51
- [
52
- @buf.slice(0, @first_cdh),
53
- data,
54
- # just before the offset of start of central dir
55
- @buf.slice(@first_cdh, (@eocd+16) - @first_cdh),
56
- # inject new offset
57
- [offset + data.bytesize].pack('V'),
58
- # the rest
59
- @buf.slice(@eocd+16+4, @buf.size)
60
- ]
104
+ HIDDEN_BLOB_VERSION = 1
105
+ HIDDEN_BLOB_HEADER_SIZE = 9 # bytes
106
+
107
+ class HiddenBlobHeader < BinData::Record
108
+ endian :little
109
+
110
+ uint32 :checksum
111
+ uint32 :len
112
+ uint8 :version, initial_value: HIDDEN_BLOB_VERSION
113
+ end
114
+
115
+ def adler32_file file, bufsiz: 1 << 16
116
+ r = nil
117
+ File.open(file, 'rb') do |f|
118
+ while (chunk = f.read bufsiz)
119
+ r = Zlib.adler32 chunk, r
120
+ end
61
121
  end
122
+ r
123
+ end
124
+
125
+ def blob_write file, dest
126
+ blob_size = File.stat(file).size
127
+
128
+ header = HiddenBlobHeader.new len: blob_size, checksum: adler32_file(file)
129
+ IO.copy_stream file, dest
130
+ dest.write header.to_binary_s
62
131
 
63
- def blob
64
- payload = ''
65
- header = {}
66
- File.open(@file) do |f|
67
- f.seek(offset-HEADER_SIZE)
68
- header = HiddenBlob.read f
69
- f.seek(offset-HEADER_SIZE-header.len)
70
- payload = f.read header.len
132
+ blob_size + header.num_bytes
133
+ end
134
+
135
+ def adler32_file_slice file, start, length, bufsiz: 1 << 16
136
+ r = nil
137
+ File.open(file, 'rb') do |f|
138
+ f.seek start
139
+ bytes_read = 0
140
+ idx = 0
141
+ finito = false
142
+ while (chunk = f.read bufsiz)
143
+ bytes_read += chunk.bytesize
144
+
145
+ if bytes_read > length
146
+ if idx == 0
147
+ chunk = chunk.byteslice(0, length)
148
+ else
149
+ chunk = chunk.byteslice(0, chunk.bytesize - (bytes_read - length))
150
+ end
151
+ finito = true
152
+ end
153
+ r = Zlib.adler32 chunk, r
154
+ break if finito
155
+ idx += 1
71
156
  end
72
- { header: header, payload: payload }
73
157
  end
158
+ r
159
+ end
160
+
161
+ def adler2hex i; "0x" + i.to_i.to_s(16); end
74
162
 
75
- def payload_valid? blob
76
- blob[:header][:checksum] == checksum(blob[:payload])
163
+ def options usage, argv_size
164
+ opt = {}; op = OptionParser.new do |o|
165
+ o.banner = "Usage: #{File.basename $0} #{usage}"
166
+ o.on("-o FILE", "output") do |arg|
167
+ opt[:output] = File.open arg, 'wb'
168
+ end
77
169
  end
170
+ op.parse!
171
+
172
+ abort op.help if ARGV.size < argv_size
173
+ opt[:output] || $stdout
78
174
  end
79
175
 
176
+ def hbh_validate header
177
+ header[:len] <= 2**32 - 70 &&
178
+ header[:version] > 0 && header[:version] <= HIDDEN_BLOB_VERSION
179
+ end
180
+
181
+ def cd_offset_start_overflow zip_file, offset
182
+ max = (2**32)-1
183
+ actual = offset + File.stat(zip_file).size + HIDDEN_BLOB_HEADER_SIZE
184
+ actual - max
185
+ end
80
186
  end
@@ -3,6 +3,20 @@
3
3
  require_relative './lib'
4
4
  include Zipography
5
5
 
6
- z = MyZip.new ARGV[0]
7
- blob = z.blob
8
- z.payload_valid?(blob) ? $stdout.write(blob[:payload]) : exit(1)
6
+ output = options 'file.zip [-o blob]', 1
7
+
8
+ eocd = eocd_parse ARGV[0], eocd_position(ARGV[0])
9
+
10
+ File.open(ARGV[0], 'rb') do |f|
11
+ f.seek eocd[:cd_offset_start] - HIDDEN_BLOB_HEADER_SIZE
12
+ header = HiddenBlobHeader.read f
13
+
14
+ # validate
15
+ exit 1 unless hbh_validate header
16
+ start_location = eocd[:cd_offset_start] - HIDDEN_BLOB_HEADER_SIZE - header.len
17
+ checksum = adler32_file_slice ARGV[0], start_location, header.len
18
+ exit 1 unless checksum == header[:checksum]
19
+
20
+ f.seek start_location
21
+ IO.copy_stream f, output, header.len
22
+ end
@@ -3,10 +3,25 @@
3
3
  require_relative './lib'
4
4
  include Zipography
5
5
 
6
- z = MyZip.new ARGV[0]
7
- blob = z.blob
6
+ eocd = eocd_parse ARGV[0], eocd_position(ARGV[0])
8
7
 
9
- puts "Payload size: #{blob[:header][:len]}"
10
- puts "Adler32: 0x" + blob[:header][:checksum].to_i.to_s(16)
11
- puts "Version: #{blob[:header][:version]}"
12
- puts "Valid: #{z.payload_valid?(blob)}"
8
+ header = File.open(ARGV[0], 'rb') do |f|
9
+ f.seek eocd[:cd_offset_start] - HIDDEN_BLOB_HEADER_SIZE
10
+ HiddenBlobHeader.read f
11
+ end
12
+
13
+ puts "Payload size: #{header[:len]}"
14
+ puts "Adler32: " + adler2hex(header[:checksum])
15
+ puts "Blob version: #{header[:version]}"
16
+
17
+ unless hbh_validate header
18
+ puts "Error: incompatible version or invalid payload size"
19
+ exit 1
20
+ end
21
+
22
+ checksum = adler32_file_slice ARGV[0], eocd[:cd_offset_start]-HIDDEN_BLOB_HEADER_SIZE-header.len, header.len
23
+
24
+ if checksum != header[:checksum]
25
+ puts "Error: invalid checksum #{adler2hex(checksum)}"
26
+ exit 1
27
+ end
@@ -3,8 +3,28 @@
3
3
  require_relative './lib'
4
4
  include Zipography
5
5
 
6
- abort "Usage: #{File.basename __FILE__} old.zip blob > new.zip" if ARGV.size < 2
6
+ output = options 'orig.zip blob [-o new.zip]', 2
7
7
 
8
- z = MyZip.new ARGV[0]
9
- data = blob_make ARGV[1]
10
- z.repack(data).each {|buf| $stdout.write buf }
8
+ # 0. get EOCD
9
+ eocd_pos = eocd_position ARGV[0]
10
+ eocd = eocd_parse ARGV[0], eocd_pos
11
+
12
+ # 1. check a would-be zip size
13
+ oo = cd_offset_start_overflow ARGV[1], eocd[:cd_offset_start]
14
+ fail "`#{ARGV[1]}` is too big: #{oo} extra byte(s)" if oo > 0
15
+
16
+ File.open(ARGV[0], 'rb') do |f|
17
+ # 2. Copy everything before CDH
18
+ IO.copy_stream f, output, eocd[:cd_offset_start]
19
+
20
+ # 3. Inject our blob
21
+ blob_size = blob_write ARGV[1], output
22
+
23
+ # 4. Copy CDH
24
+ f.seek eocd[:cd_offset_start]
25
+ IO.copy_stream f, output, eocd_pos - eocd[:cd_offset_start]
26
+
27
+ # 5. Add an updated EOCD
28
+ eocd[:cd_offset_start] += blob_size
29
+ output.write eocd.to_binary_s
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zipography
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Gromnitsky
8
8
  autorequire:
9
9
  bindir: "."
10
10
  cert_chain: []
11
- date: 2020-08-22 00:00:00.000000000 Z
11
+ date: 2020-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bindata