rubyzip 2.4.1 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +549 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +250 -44
  5. data/Rakefile +15 -13
  6. data/lib/rubyzip.rb +3 -0
  7. data/lib/zip/central_directory.rb +194 -125
  8. data/lib/zip/compressor.rb +3 -1
  9. data/lib/zip/constants.rb +30 -21
  10. data/lib/zip/crypto/aes_encryption.rb +213 -0
  11. data/lib/zip/crypto/decrypted_io.rb +27 -15
  12. data/lib/zip/crypto/encryption.rb +13 -2
  13. data/lib/zip/crypto/null_encryption.rb +5 -13
  14. data/lib/zip/crypto/traditional_encryption.rb +13 -7
  15. data/lib/zip/decompressor.rb +4 -3
  16. data/lib/zip/deflater.rb +12 -8
  17. data/lib/zip/dirtyable.rb +32 -0
  18. data/lib/zip/dos_time.rb +64 -6
  19. data/lib/zip/entry.rb +432 -270
  20. data/lib/zip/entry_set.rb +11 -9
  21. data/lib/zip/errors.rb +152 -16
  22. data/lib/zip/extra_field/aes.rb +50 -0
  23. data/lib/zip/extra_field/generic.rb +10 -11
  24. data/lib/zip/extra_field/ntfs.rb +6 -4
  25. data/lib/zip/extra_field/old_unix.rb +5 -3
  26. data/lib/zip/extra_field/universal_time.rb +3 -1
  27. data/lib/zip/extra_field/unix.rb +7 -5
  28. data/lib/zip/extra_field/unknown.rb +35 -0
  29. data/lib/zip/extra_field/zip64.rb +19 -5
  30. data/lib/zip/extra_field.rb +25 -23
  31. data/lib/zip/file.rb +261 -262
  32. data/lib/zip/file_split.rb +91 -0
  33. data/lib/zip/filesystem/dir.rb +86 -0
  34. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  35. data/lib/zip/filesystem/file.rb +263 -0
  36. data/lib/zip/filesystem/file_stat.rb +110 -0
  37. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  38. data/lib/zip/filesystem.rb +27 -596
  39. data/lib/zip/inflater.rb +13 -10
  40. data/lib/zip/input_stream.rb +102 -61
  41. data/lib/zip/ioextras/abstract_input_stream.rb +156 -58
  42. data/lib/zip/ioextras/abstract_output_stream.rb +15 -3
  43. data/lib/zip/ioextras/fake_io.rb +39 -0
  44. data/lib/zip/ioextras.rb +12 -16
  45. data/lib/zip/null_compressor.rb +3 -1
  46. data/lib/zip/null_decompressor.rb +6 -3
  47. data/lib/zip/null_input_stream.rb +3 -1
  48. data/lib/zip/output_stream.rb +76 -64
  49. data/lib/zip/pass_thru_compressor.rb +6 -3
  50. data/lib/zip/pass_thru_decompressor.rb +12 -9
  51. data/lib/zip/streamable_directory.rb +3 -1
  52. data/lib/zip/streamable_stream.rb +4 -1
  53. data/lib/zip/version.rb +4 -1
  54. data/lib/zip.rb +28 -23
  55. data/rubyzip.gemspec +39 -0
  56. data/samples/example.rb +8 -3
  57. data/samples/example_filesystem.rb +3 -2
  58. data/samples/example_recursive.rb +3 -1
  59. data/samples/gtk_ruby_zip.rb +5 -3
  60. data/samples/qtzip.rb +7 -6
  61. data/samples/write_simple.rb +2 -1
  62. data/samples/zipfind.rb +1 -0
  63. metadata +88 -52
  64. data/TODO +0 -15
  65. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
@@ -1,45 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require_relative 'dirtyable'
6
+
1
7
  module Zip
2
- class CentralDirectory
3
- include Enumerable
8
+ class CentralDirectory # :nodoc:
9
+ extend Forwardable
10
+ include Dirtyable
11
+
12
+ END_OF_CD_SIG = 0x06054b50
13
+ ZIP64_END_OF_CD_SIG = 0x06064b50
14
+ ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
4
15
 
5
- END_OF_CDS = 0x06054b50
6
- ZIP64_END_OF_CDS = 0x06064b50
7
- ZIP64_EOCD_LOCATOR = 0x07064b50
8
- MAX_END_OF_CDS_SIZE = 65_536 + 18
9
16
  STATIC_EOCD_SIZE = 22
17
+ ZIP64_STATIC_EOCD_SIZE = 56
18
+ ZIP64_EOCD_LOC_SIZE = 20
19
+ MAX_FILE_COMMENT_SIZE = (1 << 16) - 1
20
+ MAX_END_OF_CD_SIZE =
21
+ MAX_FILE_COMMENT_SIZE + STATIC_EOCD_SIZE + ZIP64_EOCD_LOC_SIZE
10
22
 
11
- attr_reader :comment
23
+ attr_accessor :comment
12
24
 
13
- # Returns an Enumerable containing the entries.
14
- def entries
15
- @entry_set.entries
16
- end
25
+ def_delegators :@entry_set,
26
+ :<<, :delete, :each, :entries, :find_entry, :glob,
27
+ :include?, :size
28
+
29
+ mark_dirty :<<, :comment=, :delete
17
30
 
18
- def initialize(entries = EntrySet.new, comment = '') #:nodoc:
19
- super()
31
+ def initialize(entries = EntrySet.new, comment = '') # :nodoc:
32
+ super(dirty_on_create: false)
20
33
  @entry_set = entries.kind_of?(EntrySet) ? entries : EntrySet.new(entries)
21
34
  @comment = comment
22
35
  end
23
36
 
24
- def write_to_stream(io) #:nodoc:
37
+ def read_from_stream(io)
38
+ read_eocds(io)
39
+ read_central_directory_entries(io)
40
+ end
41
+
42
+ def write_to_stream(io, suppress_extra_fields: false) # :nodoc:
25
43
  cdir_offset = io.tell
26
- @entry_set.each { |entry| entry.write_c_dir_entry(io) }
44
+ @entry_set.each do |entry|
45
+ entry.write_c_dir_entry(io, suppress_extra_fields: suppress_extra_fields)
46
+ end
27
47
  eocd_offset = io.tell
28
48
  cdir_size = eocd_offset - cdir_offset
29
- if ::Zip.write_zip64_support
30
- need_zip64_eocd = cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF
31
- need_zip64_eocd ||= @entry_set.any? { |entry| entry.extra['Zip64'] }
32
- if need_zip64_eocd
33
- write_64_e_o_c_d(io, cdir_offset, cdir_size)
34
- write_64_eocd_locator(io, eocd_offset)
35
- end
49
+ if Zip.write_zip64_support &&
50
+ (cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF)
51
+ write_64_e_o_c_d(io, cdir_offset, cdir_size)
52
+ write_64_eocd_locator(io, eocd_offset)
36
53
  end
37
54
  write_e_o_c_d(io, cdir_offset, cdir_size)
38
55
  end
39
56
 
40
- def write_e_o_c_d(io, offset, cdir_size) #:nodoc:
57
+ # Reads the End of Central Directory Record (and the Zip64 equivalent if
58
+ # needs be) and returns the number of entries in the archive. This is a
59
+ # convenience method that avoids reading in all of the entry data to get a
60
+ # very quick entry count.
61
+ def count_entries(io)
62
+ read_eocds(io)
63
+ @size
64
+ end
65
+
66
+ def ==(other) # :nodoc:
67
+ return false unless other.kind_of?(CentralDirectory)
68
+
69
+ @entry_set.entries.sort == other.entries.sort && comment == other.comment
70
+ end
71
+
72
+ private
73
+
74
+ def write_e_o_c_d(io, offset, cdir_size) # :nodoc:
41
75
  tmp = [
42
- END_OF_CDS,
76
+ END_OF_CD_SIG,
43
77
  0, # @numberOfThisDisk
44
78
  0, # @numberOfDiskWithStartOfCDir
45
79
  @entry_set ? [@entry_set.size, 0xFFFF].min : 0,
@@ -52,11 +86,9 @@ module Zip
52
86
  io << @comment
53
87
  end
54
88
 
55
- private :write_e_o_c_d
56
-
57
- def write_64_e_o_c_d(io, offset, cdir_size) #:nodoc:
89
+ def write_64_e_o_c_d(io, offset, cdir_size) # :nodoc:
58
90
  tmp = [
59
- ZIP64_END_OF_CDS,
91
+ ZIP64_END_OF_CD_SIG,
60
92
  44, # size of zip64 end of central directory record (excludes signature and field itself)
61
93
  VERSION_MADE_BY,
62
94
  VERSION_NEEDED_TO_EXTRACT_ZIP64,
@@ -70,11 +102,9 @@ module Zip
70
102
  io << tmp.pack('VQ<vvVVQ<Q<Q<Q<')
71
103
  end
72
104
 
73
- private :write_64_e_o_c_d
74
-
75
105
  def write_64_eocd_locator(io, zip64_eocd_offset)
76
106
  tmp = [
77
- ZIP64_EOCD_LOCATOR,
107
+ ZIP64_EOCD_LOCATOR_SIG,
78
108
  0, # number of disk containing the start of zip64 eocd record
79
109
  zip64_eocd_offset, # offset of the start of zip64 eocd record in its disk
80
110
  1 # total number of disks
@@ -82,127 +112,166 @@ module Zip
82
112
  io << tmp.pack('VVQ<V')
83
113
  end
84
114
 
85
- private :write_64_eocd_locator
86
-
87
- def read_64_e_o_c_d(buf) #:nodoc:
88
- buf = get_64_e_o_c_d(buf)
89
- @size_of_zip64_e_o_c_d = Entry.read_zip_64_long(buf)
90
- @version_made_by = Entry.read_zip_short(buf)
91
- @version_needed_for_extract = Entry.read_zip_short(buf)
92
- @number_of_this_disk = Entry.read_zip_long(buf)
93
- @number_of_disk_with_start_of_cdir = Entry.read_zip_long(buf)
94
- @total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_64_long(buf)
95
- @size = Entry.read_zip_64_long(buf)
96
- @size_in_bytes = Entry.read_zip_64_long(buf)
97
- @cdir_offset = Entry.read_zip_64_long(buf)
98
- @zip_64_extensible = buf.slice!(0, buf.bytesize)
99
- raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
100
- end
101
-
102
- def read_e_o_c_d(buf) #:nodoc:
103
- buf = get_e_o_c_d(buf)
104
- @number_of_this_disk = Entry.read_zip_short(buf)
105
- @number_of_disk_with_start_of_cdir = Entry.read_zip_short(buf)
106
- @total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_short(buf)
107
- @size = Entry.read_zip_short(buf)
108
- @size_in_bytes = Entry.read_zip_long(buf)
109
- @cdir_offset = Entry.read_zip_long(buf)
110
- comment_length = Entry.read_zip_short(buf)
111
- @comment = if comment_length.to_i <= 0
112
- buf.slice!(0, buf.size)
113
- else
114
- buf.read(comment_length)
115
- end
116
- raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
117
- end
118
-
119
- def read_central_directory_entries(io) #:nodoc:
115
+ def unpack_64_e_o_c_d(buffer) # :nodoc:
116
+ _, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
117
+ size_of_zip64_e_o_c_d,
118
+ _version_made_by,
119
+ _version_needed_for_extract,
120
+ _number_of_this_disk,
121
+ _number_of_disk_with_start_of_cdir,
122
+ _total_number_of_entries_in_cdir_on_this_disk,
123
+ @size,
124
+ size_in_bytes,
125
+ @cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
126
+
127
+ validate_size!(@size, size_in_bytes)
128
+
129
+ zip64_extensible_data_size =
130
+ size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
131
+ @zip64_extensible_data = if zip64_extensible_data_size.zero?
132
+ ''
133
+ else
134
+ buffer.slice(
135
+ ZIP64_STATIC_EOCD_SIZE,
136
+ zip64_extensible_data_size
137
+ )
138
+ end
139
+ end
140
+
141
+ def unpack_64_eocd_locator(buffer) # :nodoc:
142
+ _, # ZIP64_EOCD_LOCATOR_SIG. We know we have this at this point.
143
+ _, zip64_eocd_offset, = buffer.unpack('VVQ<V')
144
+
145
+ zip64_eocd_offset
146
+ end
147
+
148
+ # Unpack the EOCD and return a boolean indicating whether this header is
149
+ # complete without needing Zip64 extensions.
150
+ def unpack_e_o_c_d(buffer) # :nodoc:
151
+ _, # END_OF_CD_SIG. We know we have this at this point.
152
+ number_of_this_disk,
153
+ number_of_disk_with_start_of_cdir,
154
+ total_number_of_entries_in_cdir_on_this_disk,
155
+ @size,
156
+ size_in_bytes,
157
+ @cdir_offset,
158
+ comment_length = buffer.unpack('VvvvvVVv')
159
+
160
+ complete = !([number_of_this_disk, number_of_disk_with_start_of_cdir,
161
+ total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
162
+ size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
163
+
164
+ validate_size!(@size, size_in_bytes) if complete
165
+
166
+ @comment = if comment_length.positive?
167
+ buffer.slice(STATIC_EOCD_SIZE, comment_length)
168
+ else
169
+ ''
170
+ end
171
+
172
+ complete
173
+ end
174
+
175
+ def read_central_directory_entries(io) # :nodoc:
176
+ # `StringIO` doesn't raise `EINVAL` if you seek beyond the current end,
177
+ # so we need to catch that *and* query `io#eof?` here.
178
+ eof = false
120
179
  begin
121
180
  io.seek(@cdir_offset, IO::SEEK_SET)
122
181
  rescue Errno::EINVAL
123
- raise Error, 'Zip consistency problem while reading central directory entry'
182
+ eof = true
124
183
  end
184
+ raise Error, 'Zip consistency problem while reading central directory entry' if eof || io.eof?
185
+
125
186
  @entry_set = EntrySet.new
126
187
  @size.times do
127
- @entry_set << Entry.read_c_dir_entry(io)
188
+ entry = Entry.read_c_dir_entry(io)
189
+ break unless entry
190
+
191
+ offset = if entry.zip64?
192
+ entry.extra[:zip64].relative_header_offset
193
+ else
194
+ entry.local_header_offset
195
+ end
196
+
197
+ unless offset.nil?
198
+ io_save = io.tell
199
+ io.seek(offset, IO::SEEK_SET)
200
+ entry.read_extra_field(read_local_extra_field(io), local: true)
201
+ io.seek(io_save, IO::SEEK_SET)
202
+ end
203
+
204
+ @entry_set << entry
128
205
  end
129
206
  end
130
207
 
131
- def read_from_stream(io) #:nodoc:
132
- buf = start_buf(io)
133
- if zip64_file?(buf)
134
- read_64_e_o_c_d(buf)
135
- else
136
- read_e_o_c_d(buf)
137
- end
138
- read_central_directory_entries(io)
208
+ def read_local_extra_field(io)
209
+ buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
210
+ return '' unless buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
211
+
212
+ head, _, _, _, _, _, _, _, _, _, n_len, e_len = buf.unpack('VCCvvvvVVVvv')
213
+ return '' unless head == ::Zip::LOCAL_ENTRY_SIGNATURE
214
+
215
+ io.seek(n_len, IO::SEEK_CUR) # Skip over the entry name.
216
+ io.read(e_len)
139
217
  end
140
218
 
141
- def get_e_o_c_d(buf) #:nodoc:
142
- sig_index = buf.rindex([END_OF_CDS].pack('V'))
143
- raise Error, 'Zip end of central directory signature not found' unless sig_index
219
+ def read_eocds(io) # :nodoc:
220
+ base_location, data = eocd_data(io)
221
+
222
+ eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
223
+ raise Error, 'Zip end of central directory signature not found' unless eocd_location
144
224
 
145
- buf = buf.slice!((sig_index + 4)..(buf.bytesize))
225
+ # Parse the EOCD and return if it is complete without Zip64 extensions.
226
+ return if unpack_e_o_c_d(data.slice(eocd_location..-1))
146
227
 
147
- def buf.read(count)
148
- slice!(0, count)
228
+ # Need to read in the Zip64 EOCD locator and then the Zip64 EOCD.
229
+ zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'), eocd_location)
230
+ unless zip64_eocd_locator
231
+ raise Error, 'Zip64 end of central directory locator signature expected but not found'
149
232
  end
150
233
 
151
- buf
152
- end
234
+ # Do we already have the Zip64 EOCD in the data we've read?
235
+ zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'), zip64_eocd_locator)
236
+
237
+ zip64_eocd_data =
238
+ if zip64_eocd_location
239
+ # Yes.
240
+ data.slice(zip64_eocd_location..zip64_eocd_locator)
241
+ else
242
+ # No. Read its location from the locator and then read it in.
243
+ zip64_eocd_location = unpack_64_eocd_locator(
244
+ data.slice(zip64_eocd_locator..eocd_location)
245
+ )
246
+ unless zip64_eocd_location
247
+ raise Error, 'Zip64 end of central directory signature not found'
248
+ end
249
+
250
+ io.seek(zip64_eocd_location, IO::SEEK_SET)
251
+ io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
252
+ end
153
253
 
154
- def zip64_file?(buf)
155
- buf.rindex([ZIP64_END_OF_CDS].pack('V')) && buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
254
+ # Finally, unpack the Zip64 EOCD.
255
+ unpack_64_e_o_c_d(zip64_eocd_data)
156
256
  end
157
257
 
158
- def start_buf(io)
258
+ def eocd_data(io)
159
259
  begin
160
- io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
260
+ io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
161
261
  rescue Errno::EINVAL
162
262
  io.seek(0, IO::SEEK_SET)
163
263
  end
164
- io.read
165
- end
166
264
 
167
- def get_64_e_o_c_d(buf) #:nodoc:
168
- zip_64_start = buf.rindex([ZIP64_END_OF_CDS].pack('V'))
169
- raise Error, 'Zip64 end of central directory signature not found' unless zip_64_start
170
-
171
- zip_64_locator = buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
172
- raise Error, 'Zip64 end of central directory signature locator not found' unless zip_64_locator
173
-
174
- buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
175
-
176
- def buf.read(count)
177
- slice!(0, count)
178
- end
179
-
180
- buf
181
- end
182
-
183
- # For iterating over the entries.
184
- def each(&a_proc)
185
- @entry_set.each(&a_proc)
186
- end
187
-
188
- # Returns the number of entries in the central directory (and
189
- # consequently in the zip archive).
190
- def size
191
- @entry_set.size
265
+ [io.tell, io.read]
192
266
  end
193
267
 
194
- def self.read_from_stream(io) #:nodoc:
195
- cdir = new
196
- cdir.read_from_stream(io)
197
- cdir
198
- rescue Error
199
- nil
200
- end
268
+ def validate_size!(size, size_in_bytes)
269
+ return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
201
270
 
202
- def ==(other) #:nodoc:
203
- return false unless other.kind_of?(CentralDirectory)
271
+ error = EntryNumberMismatchError.new(size, size_in_bytes)
272
+ raise error if Zip.validate_declared_number_of_entries
204
273
 
205
- @entry_set.entries.sort == other.entries.sort && comment == other.comment
274
+ warn error.message
206
275
  end
207
276
  end
208
277
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class Compressor #:nodoc:all
4
+ class Compressor # :nodoc:all
3
5
  def finish; end
4
6
  end
5
7
  end
data/lib/zip/constants.rb CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
4
+ # :stopdoc:
5
+
2
6
  RUNNING_ON_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/i
3
7
 
4
8
  CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
@@ -10,6 +14,9 @@ module Zip
10
14
  VERSION_MADE_BY = 52 # this library's version
11
15
  VERSION_NEEDED_TO_EXTRACT = 20
12
16
  VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
17
+ VERSION_NEEDED_TO_EXTRACT_AES = 51
18
+
19
+ SPLIT_FILE_SIGNATURE = 0x08074b50
13
20
 
14
21
  FILE_TYPE_FILE = 0o10
15
22
  FILE_TYPE_DIR = 0o04
@@ -38,27 +45,27 @@ module Zip
38
45
  FSTYPE_ATHEOS = 30
39
46
 
40
47
  FSTYPES = {
41
- FSTYPE_FAT => 'FAT'.freeze,
42
- FSTYPE_AMIGA => 'Amiga'.freeze,
43
- FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
44
- FSTYPE_UNIX => 'Unix'.freeze,
45
- FSTYPE_VM_CMS => 'VM/CMS'.freeze,
46
- FSTYPE_ATARI => 'Atari ST'.freeze,
47
- FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
48
- FSTYPE_MAC => 'Macintosh'.freeze,
49
- FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
50
- FSTYPE_CPM => 'CP/M'.freeze,
51
- FSTYPE_TOPS20 => 'TOPS-20'.freeze,
52
- FSTYPE_NTFS => 'NTFS'.freeze,
53
- FSTYPE_QDOS => 'SMS/QDOS'.freeze,
54
- FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
55
- FSTYPE_VFAT => 'Win32 VFAT'.freeze,
56
- FSTYPE_MVS => 'MVS'.freeze,
57
- FSTYPE_BEOS => 'BeOS'.freeze,
58
- FSTYPE_TANDEM => 'Tandem NSK'.freeze,
59
- FSTYPE_THEOS => 'Theos'.freeze,
60
- FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
61
- FSTYPE_ATHEOS => 'AtheOS'.freeze
48
+ FSTYPE_FAT => 'FAT',
49
+ FSTYPE_AMIGA => 'Amiga',
50
+ FSTYPE_VMS => 'VMS (Vax or Alpha AXP)',
51
+ FSTYPE_UNIX => 'Unix',
52
+ FSTYPE_VM_CMS => 'VM/CMS',
53
+ FSTYPE_ATARI => 'Atari ST',
54
+ FSTYPE_HPFS => 'OS/2 or NT HPFS',
55
+ FSTYPE_MAC => 'Macintosh',
56
+ FSTYPE_Z_SYSTEM => 'Z-System',
57
+ FSTYPE_CPM => 'CP/M',
58
+ FSTYPE_TOPS20 => 'TOPS-20',
59
+ FSTYPE_NTFS => 'NTFS',
60
+ FSTYPE_QDOS => 'SMS/QDOS',
61
+ FSTYPE_ACORN => 'Acorn RISC OS',
62
+ FSTYPE_VFAT => 'Win32 VFAT',
63
+ FSTYPE_MVS => 'MVS',
64
+ FSTYPE_BEOS => 'BeOS',
65
+ FSTYPE_TANDEM => 'Tandem NSK',
66
+ FSTYPE_THEOS => 'Theos',
67
+ FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)',
68
+ FSTYPE_ATHEOS => 'AtheOS'
62
69
  }.freeze
63
70
 
64
71
  COMPRESSION_METHOD_STORE = 0
@@ -112,4 +119,6 @@ module Zip
112
119
  COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
113
120
  COMPRESSION_METHOD_AES => 'AES encryption'
114
121
  }.freeze
122
+
123
+ # :startdoc:
115
124
  end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Zip
6
+ module AESEncryption # :nodoc:
7
+ VERIFIER_LENGTH = 2
8
+ BLOCK_SIZE = 16
9
+ AUTHENTICATION_CODE_LENGTH = 10
10
+
11
+ VERSION_AE_1 = 0x01
12
+ VERSION_AE_2 = 0x02
13
+
14
+ VERSIONS = [
15
+ VERSION_AE_1,
16
+ VERSION_AE_2
17
+ ].freeze
18
+
19
+ STRENGTH_128_BIT = 0x01
20
+ STRENGTH_192_BIT = 0x02
21
+ STRENGTH_256_BIT = 0x03
22
+
23
+ STRENGTHS = [
24
+ STRENGTH_128_BIT,
25
+ STRENGTH_192_BIT,
26
+ STRENGTH_256_BIT
27
+ ].freeze
28
+
29
+ BITS = {
30
+ STRENGTH_128_BIT => 128,
31
+ STRENGTH_192_BIT => 192,
32
+ STRENGTH_256_BIT => 256
33
+ }.freeze
34
+
35
+ KEY_LENGTHS = {
36
+ STRENGTH_128_BIT => 16,
37
+ STRENGTH_192_BIT => 24,
38
+ STRENGTH_256_BIT => 32
39
+ }.freeze
40
+
41
+ SALT_LENGTHS = {
42
+ STRENGTH_128_BIT => 8,
43
+ STRENGTH_192_BIT => 12,
44
+ STRENGTH_256_BIT => 16
45
+ }.freeze
46
+
47
+ def initialize(password, strength)
48
+ # Loaded here rather than at the top of the file so that `require 'zip'`
49
+ # does not pull in openssl. Only AES-encrypted archives need it, and it
50
+ # is the single largest cost of loading this library.
51
+ require 'openssl'
52
+
53
+ @password = password
54
+ @strength = strength
55
+ @bits = BITS[@strength]
56
+ @key_length = KEY_LENGTHS[@strength]
57
+ @salt_length = SALT_LENGTHS[@strength]
58
+ end
59
+
60
+ def header_bytesize
61
+ @salt_length + VERIFIER_LENGTH
62
+ end
63
+
64
+ def gp_flags
65
+ 0x0001
66
+ end
67
+
68
+ private
69
+
70
+ # Derive the encryption key, HMAC key and password-verification value
71
+ # from the password and a salt, as specified by the WinZip AES format.
72
+ def derive_keys(salt)
73
+ raise Error, "Unsupported encryption AES-#{@bits}" unless STRENGTHS.include? @strength
74
+
75
+ key_material = OpenSSL::KDF.pbkdf2_hmac(
76
+ @password,
77
+ salt: salt,
78
+ iterations: 1000,
79
+ length: (2 * @key_length) + VERIFIER_LENGTH,
80
+ hash: 'sha1'
81
+ )
82
+
83
+ [
84
+ key_material[0...@key_length],
85
+ key_material[@key_length...(2 * @key_length)],
86
+ key_material[-VERIFIER_LENGTH..]
87
+ ]
88
+ end
89
+ end
90
+
91
+ class AESEncrypter < Encrypter # :nodoc:
92
+ include AESEncryption
93
+
94
+ def header(_mtime)
95
+ @salt + @pwd_verify
96
+ end
97
+
98
+ # `Deflater`/`PassThruCompressor` call this with whatever, arbitrarily
99
+ # sized (and not necessarily block-aligned) buffer they happen to have
100
+ # flushed, potentially many times per entry. Only whole 16-byte blocks
101
+ # are actually run through the cipher here; any trailing partial block
102
+ # is buffered in `@pending` until either more data completes it or
103
+ # `trailer` forces the final flush. This keeps the CTR counter aligned
104
+ # with the true byte offset in the plaintext stream regardless of how
105
+ # callers happen to chunk their writes.
106
+ def encrypt(data)
107
+ @pending << data
108
+ encrypted_data = encrypt_blocks
109
+ @hmac.update(encrypted_data)
110
+ encrypted_data
111
+ end
112
+
113
+ def data_descriptor(*)
114
+ ''
115
+ end
116
+
117
+ def trailer
118
+ encrypted_data = encrypt_blocks(final: true)
119
+ @hmac.update(encrypted_data)
120
+ encrypted_data + @hmac.digest[0...AUTHENTICATION_CODE_LENGTH]
121
+ end
122
+
123
+ def crc(_computed_crc)
124
+ 0
125
+ end
126
+
127
+ def reset!
128
+ @salt = SecureRandom.random_bytes(@salt_length)
129
+ enc_key, enc_hmac_key, @pwd_verify = derive_keys(@salt)
130
+
131
+ @counter = 0
132
+ @pending = +''.b
133
+ @cipher = OpenSSL::Cipher::AES.new(@bits, :CTR)
134
+ @cipher.encrypt
135
+ @cipher.key = enc_key
136
+ @hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
137
+ end
138
+
139
+ def prepare_entry(entry)
140
+ entry.prep_aes_extra(AESEncryption::VERSION_AE_2, @strength)
141
+ end
142
+
143
+ private
144
+
145
+ def encrypt_blocks(final: false)
146
+ length = final ? @pending.bytesize : (@pending.bytesize / BLOCK_SIZE) * BLOCK_SIZE
147
+ return '' if length.zero?
148
+
149
+ data = @pending.slice!(0, length)
150
+ encrypted_data = +''.b
151
+ offset = 0
152
+
153
+ while offset < data.bytesize
154
+ @cipher.iv = [@counter + 1].pack('Vx12')
155
+ encrypted_data << @cipher.update(data[offset, BLOCK_SIZE])
156
+ @counter += 1
157
+ offset += BLOCK_SIZE
158
+ end
159
+
160
+ # JRuby requires finalization of the cipher when the last block fed to
161
+ # it is a partial one. This is a bug, as noted in
162
+ # jruby/jruby-openssl#182 and jruby/jruby-openssl#183.
163
+ encrypted_data << @cipher.final if final && defined?(JRUBY_VERSION)
164
+ encrypted_data
165
+ end
166
+ end
167
+
168
+ class AESDecrypter < Decrypter # :nodoc:
169
+ include AESEncryption
170
+
171
+ def decrypt(encrypted_data)
172
+ @hmac.update(encrypted_data)
173
+
174
+ idx = 0
175
+ decrypted_data = +''
176
+ amount_to_read = encrypted_data.size
177
+
178
+ while amount_to_read.positive?
179
+ @cipher.iv = [@counter + 1].pack('Vx12')
180
+ begin_index = BLOCK_SIZE * idx
181
+ end_index = begin_index + [BLOCK_SIZE, amount_to_read].min
182
+ decrypted_data << @cipher.update(encrypted_data[begin_index...end_index])
183
+ amount_to_read -= BLOCK_SIZE
184
+ @counter += 1
185
+ idx += 1
186
+ end
187
+
188
+ # JRuby requires finalization of the cipher. This is a bug, as noted in
189
+ # jruby/jruby-openssl#182 and jruby/jruby-openssl#183.
190
+ decrypted_data << @cipher.final if defined?(JRUBY_VERSION)
191
+ decrypted_data
192
+ end
193
+
194
+ def reset!(header)
195
+ salt = header[0...@salt_length]
196
+ pwd_verify = header[-VERIFIER_LENGTH..]
197
+ enc_key, enc_hmac_key, enc_pwd_verify = derive_keys(salt)
198
+
199
+ raise Error, 'Bad password' if enc_pwd_verify != pwd_verify
200
+
201
+ @counter = 0
202
+ @cipher = OpenSSL::Cipher::AES.new(@bits, :CTR)
203
+ @cipher.decrypt
204
+ @cipher.key = enc_key
205
+ @hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
206
+ end
207
+
208
+ def check_integrity!(io)
209
+ auth_code = io.read(AUTHENTICATION_CODE_LENGTH)
210
+ raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
211
+ end
212
+ end
213
+ end