rubyzip 3.0.2 → 3.2.2
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.
- checksums.yaml +4 -4
- data/Changelog.md +49 -0
- data/README.md +64 -13
- data/Rakefile +4 -6
- data/lib/zip/central_directory.rb +44 -36
- data/lib/zip/crypto/aes_encryption.rb +120 -0
- data/lib/zip/crypto/decrypted_io.rb +17 -13
- data/lib/zip/crypto/null_encryption.rb +0 -10
- data/lib/zip/crypto/traditional_encryption.rb +2 -0
- data/lib/zip/dos_time.rb +3 -2
- data/lib/zip/entry.rb +52 -28
- data/lib/zip/extra_field/aes.rb +50 -0
- data/lib/zip/extra_field/generic.rb +7 -1
- data/lib/zip/extra_field/unknown.rb +3 -1
- data/lib/zip/extra_field/zip64.rb +7 -0
- data/lib/zip/extra_field.rb +12 -4
- data/lib/zip/file.rb +26 -22
- data/lib/zip/filesystem/file.rb +7 -6
- data/lib/zip/filesystem/file_stat.rb +4 -4
- data/lib/zip/inflater.rb +4 -3
- data/lib/zip/input_stream.rb +31 -12
- data/lib/zip/ioextras/abstract_input_stream.rb +3 -2
- data/lib/zip/null_decompressor.rb +3 -2
- data/lib/zip/output_stream.rb +13 -9
- data/lib/zip/pass_thru_decompressor.rb +4 -3
- data/lib/zip/streamable_stream.rb +1 -1
- data/lib/zip/version.rb +2 -1
- data/lib/zip.rb +2 -0
- data/rubyzip.gemspec +3 -3
- data/samples/gtk_ruby_zip.rb +1 -1
- data/samples/qtzip.rb +1 -1
- metadata +14 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67852d915e2ec168efb617d7577ac60196bf7433b3c2800981a14c5a43b939c5
|
|
4
|
+
data.tar.gz: '039b39e7e9e7f46e056da4d0070554eba081e26a4dd99e3dd47d76428628d29f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba24363c26265acbd295289685d5baa4d351a913d98de67064171c35f0055f04ac34b2e6db384af248e124b12abc505bac558205eadb2975d3d4da59178b9a54
|
|
7
|
+
data.tar.gz: b654167d21076c70ea58b6a185f4be83a6ef172d208bf4f8a8928ac82aa909773b20df462870dc833b1182f669334e18fd7283e23cf4040b770faea1a49d1e6a
|
data/Changelog.md
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
+
# 3.2.2 (2025-11-02)
|
|
2
|
+
|
|
3
|
+
- Fix reading EOCDs when header signatures are in an Entry payload. [#656](https://github.com/rubyzip/rubyzip/issues/656)
|
|
4
|
+
|
|
5
|
+
Tooling/internal:
|
|
6
|
+
|
|
7
|
+
- Stop using macos-13 runners in GitHub Actions.
|
|
8
|
+
- Update YJIT GitHub Actions runners.
|
|
9
|
+
|
|
10
|
+
# 3.2.1 (2025-10-24)
|
|
11
|
+
|
|
12
|
+
- Fix `Entry#gather_fileinfo_from_srcpath` error messages. [#654](https://github.com/rubyzip/rubyzip/issues/654)
|
|
13
|
+
|
|
14
|
+
Tooling/internal:
|
|
15
|
+
|
|
16
|
+
- Add some simple benchmarks for reading the cdir.
|
|
17
|
+
|
|
18
|
+
# 3.2.0 (2025-10-14)
|
|
19
|
+
|
|
20
|
+
- Add option to suppress extra fields. [#653](https://github.com/rubyzip/rubyzip/pull/653) (fixes [#34](https://github.com/rubyzip/rubyzip/issues/34), [#398](https://github.com/rubyzip/rubyzip/issues/398) and [#648](https://github.com/rubyzip/rubyzip/issues/648))
|
|
21
|
+
|
|
22
|
+
Tooling/internal:
|
|
23
|
+
|
|
24
|
+
- Entry: clean up reading and writing the Central Directory headers.
|
|
25
|
+
- Improve Zip64 tests for `OutputStream`.
|
|
26
|
+
- Extra fields: use symbols as indices as opposed to strings.
|
|
27
|
+
- Ensure that `Unknown` extra field has a superclass.
|
|
28
|
+
|
|
29
|
+
# 3.1.1 (2025-09-26)
|
|
30
|
+
|
|
31
|
+
- Improve the IO pipeline when decompressing. [#649](https://github.com/rubyzip/rubyzip/pull/649) (which also fixes [#647](https://github.com/rubyzip/rubyzip/issues/647))
|
|
32
|
+
|
|
33
|
+
Tooling/internal:
|
|
34
|
+
|
|
35
|
+
- Improve the `DecryptedIo` class with various updates and optimizations.
|
|
36
|
+
- Remove the `NullDecrypter` class.
|
|
37
|
+
- Properly convert the test suite to use minitest.
|
|
38
|
+
- Move all test helper code into separate files.
|
|
39
|
+
- Updates to the Actions CI, including new OS versions.
|
|
40
|
+
- Update rubocop versions and fix resultant cop failures. [#646](https://github.com/rubyzip/rubyzip/pull/646)
|
|
41
|
+
|
|
42
|
+
# 3.1.0 (2025-09-06)
|
|
43
|
+
|
|
44
|
+
- Support AES decryption. [#579](https://github.com/rubyzip/rubyzip/pull/579) and [#645](https://github.com/rubyzip/rubyzip/pull/645)
|
|
45
|
+
|
|
46
|
+
Tooling/internal:
|
|
47
|
+
|
|
48
|
+
- Add various useful zip specification documents to the repo for ease of finding them in the future. These are not included in the gem release.
|
|
49
|
+
|
|
1
50
|
# 3.0.2 (2025-08-21)
|
|
2
51
|
|
|
3
52
|
- Fix `InputStream#sysread` to handle frozen string literals. [#643](https://github.com/rubyzip/rubyzip/pull/643)
|
data/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
[](http://badge.fury.io/rb/rubyzip)
|
|
4
4
|
[](https://github.com/rubyzip/rubyzip/actions/workflows/tests.yml)
|
|
5
5
|
[](https://github.com/rubyzip/rubyzip/actions/workflows/lint.yml)
|
|
6
|
-
[](https://github.com/rubocop/rubocop)
|
|
7
|
+
[](https://qlty.sh/gh/rubyzip/projects/rubyzip)
|
|
7
8
|
[](https://coveralls.io/r/rubyzip/rubyzip?branch=master)
|
|
8
9
|
|
|
9
10
|
Rubyzip is a ruby library for reading and writing zip files.
|
|
@@ -66,6 +67,48 @@ Zip::File.open(zipfile_name, create: true) do |zipfile|
|
|
|
66
67
|
end
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
### Creating a Zip file with `Zip::OutputStream`
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
require 'rubygems'
|
|
74
|
+
require 'zip'
|
|
75
|
+
|
|
76
|
+
Zip::OutputStream.open('archive.zip') do |zos|
|
|
77
|
+
# Quick.
|
|
78
|
+
zos.put_next_entry('greeting.txt')
|
|
79
|
+
zos << 'Hello, World!'
|
|
80
|
+
|
|
81
|
+
# More control.
|
|
82
|
+
# You MUST NOT make any calls on your `Entry` after calling `put_next_entry`.
|
|
83
|
+
entry = Zip::Entry.new(nil, 'parting.txt')
|
|
84
|
+
entry.atime = Time.now
|
|
85
|
+
zos.put_next_entry(entry)
|
|
86
|
+
zos.write('TTFN')
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
You can generate a Zip archive in memory using `Zip::OutputStream.write_buffer`.
|
|
91
|
+
|
|
92
|
+
### Suppressing extra fields
|
|
93
|
+
|
|
94
|
+
If you wish to suppress extra fields from being added to your entries, you can do so by passing the `suppress_extra_fields` parameter to any of the archive opening calls within `Zip::File` or `Zip::OutputStream`, e.g.:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Suppress all extra fields.
|
|
98
|
+
Zip::File.open('archive.zip', create: true, suppress_extra_fields: true)
|
|
99
|
+
Zip::OutputStream.open('archive.zip', suppress_extra_fields: true)
|
|
100
|
+
|
|
101
|
+
# Suppress an individual extra field.
|
|
102
|
+
Zip::File.open('archive.zip', create: true, suppress_extra_fields: :zip64)
|
|
103
|
+
Zip::OutputStream.open('archive.zip', suppress_extra_fields: :zip64)
|
|
104
|
+
|
|
105
|
+
# Suppress multiple extra fields.
|
|
106
|
+
Zip::File.open('archive.zip', create: true, suppress_extra_fields: [:ntfs, :zip64])
|
|
107
|
+
Zip::OutputStream.open('archive.zip', suppress_extra_fields: [:ntfs, :zip64])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Note that there are some extra fields that cannot be suppressed at all (e.g. `:aes`), and some which will only be suppressed if it is safe to do so (e.g. `:zip64`).
|
|
111
|
+
|
|
69
112
|
### Zipping a directory recursively
|
|
70
113
|
|
|
71
114
|
Copy from [here](https://github.com/rubyzip/rubyzip/blob/9d891f7353e66052283562d3e252fe380bb4b199/samples/example_recursive.rb)
|
|
@@ -179,7 +222,7 @@ Zip::File.open('foo.zip') do |zip_file|
|
|
|
179
222
|
end
|
|
180
223
|
```
|
|
181
224
|
|
|
182
|
-
###
|
|
225
|
+
### Reading a Zip file with `Zip::InputStream`
|
|
183
226
|
|
|
184
227
|
`Zip::InputStream` can be used for faster reading of zip file content because it does not read the Central directory up front.
|
|
185
228
|
|
|
@@ -189,23 +232,23 @@ There is one exception where it can not work however, and this is if the file do
|
|
|
189
232
|
|
|
190
233
|
If `Zip::InputStream` finds such an entry in the zip archive it will raise an exception (`Zip::StreamingError`).
|
|
191
234
|
|
|
192
|
-
`Zip::InputStream` is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via `Zip::InputStream
|
|
235
|
+
`Zip::InputStream` is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via `Zip::InputStream#get_next_entry` then you should complete any such operations before the next call to `get_next_entry`.
|
|
193
236
|
|
|
194
237
|
```ruby
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
end
|
|
238
|
+
Zip::InputStream.open('file.zip') do |zip_stream|
|
|
239
|
+
while entry = zip_stream.get_next_entry
|
|
240
|
+
# All required operations on `entry` go here.
|
|
241
|
+
end
|
|
242
|
+
end # The `InputStream` is closed at the end of the block.
|
|
200
243
|
```
|
|
201
244
|
|
|
202
245
|
Any attempt to move about in a zip file opened with `Zip::InputStream` could result in the incorrect entry being accessed and/or Zlib buffer errors. If you need random access in a zip file, use `Zip::File`.
|
|
203
246
|
|
|
204
|
-
### Password Protection (
|
|
247
|
+
### Password Protection (experimental)
|
|
205
248
|
|
|
206
|
-
Rubyzip supports reading
|
|
249
|
+
Rubyzip supports reading zip files with AES encryption (version 3.1 and later), and reading and writing zip files with traditional zip encryption (a.k.a. "ZipCrypto"). Encryption is currently only available with the stream API, with either files or buffers, e.g.:
|
|
207
250
|
|
|
208
|
-
#### Version 2.x
|
|
251
|
+
#### Version 2.x (ZipCrypto only)
|
|
209
252
|
|
|
210
253
|
```ruby
|
|
211
254
|
# Writing.
|
|
@@ -224,9 +267,17 @@ Zip::InputStream.open(buffer, 0, dec) do |input|
|
|
|
224
267
|
end
|
|
225
268
|
```
|
|
226
269
|
|
|
227
|
-
#### Version 3.x
|
|
270
|
+
#### Version 3.x (AES reading and ZipCrypto read/write)
|
|
228
271
|
|
|
229
272
|
```ruby
|
|
273
|
+
# Reading AES, version 3.1 and later.
|
|
274
|
+
dec = Zip::AESDecrypter.new('password', Zip::AESEncryption::STRENGTH_256_BIT)
|
|
275
|
+
Zip::InputStream.open('aes-encrypted-file.zip', decrypter: dec) do |input|
|
|
276
|
+
entry = input.get_next_entry
|
|
277
|
+
puts "Contents of '#{entry.name}':"
|
|
278
|
+
puts input.read
|
|
279
|
+
end
|
|
280
|
+
|
|
230
281
|
# Writing.
|
|
231
282
|
enc = Zip::TraditionalEncrypter.new('password')
|
|
232
283
|
buffer = Zip::OutputStream.write_buffer(encrypter: enc) do |output|
|
|
@@ -243,7 +294,7 @@ Zip::InputStream.open(buffer, decrypter: dec) do |input|
|
|
|
243
294
|
end
|
|
244
295
|
```
|
|
245
296
|
|
|
246
|
-
_This is an
|
|
297
|
+
_This is an evolving feature and the interface for encryption may change in future versions._
|
|
247
298
|
|
|
248
299
|
## Known issues
|
|
249
300
|
|
data/Rakefile
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks'
|
|
4
|
-
require '
|
|
4
|
+
require 'minitest/test_task'
|
|
5
5
|
require 'rdoc/task'
|
|
6
6
|
require 'rubocop/rake_task'
|
|
7
7
|
|
|
8
8
|
task default: :test
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
test.
|
|
12
|
-
test.
|
|
13
|
-
test.pattern = 'test/**/*_test.rb'
|
|
14
|
-
test.verbose = true
|
|
10
|
+
Minitest::TestTask.create do |test|
|
|
11
|
+
test.framework = 'require "simplecov"'
|
|
12
|
+
test.test_globs = 'test/**/*_test.rb'
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
RDoc::Task.new do |rdoc|
|
|
@@ -39,9 +39,11 @@ module Zip
|
|
|
39
39
|
read_central_directory_entries(io)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def write_to_stream(io) # :nodoc:
|
|
42
|
+
def write_to_stream(io, suppress_extra_fields: false) # :nodoc:
|
|
43
43
|
cdir_offset = io.tell
|
|
44
|
-
@entry_set.each
|
|
44
|
+
@entry_set.each do |entry|
|
|
45
|
+
entry.write_c_dir_entry(io, suppress_extra_fields: suppress_extra_fields)
|
|
46
|
+
end
|
|
45
47
|
eocd_offset = io.tell
|
|
46
48
|
cdir_size = eocd_offset - cdir_offset
|
|
47
49
|
if Zip.write_zip64_support &&
|
|
@@ -141,28 +143,27 @@ module Zip
|
|
|
141
143
|
zip64_eocd_offset
|
|
142
144
|
end
|
|
143
145
|
|
|
144
|
-
|
|
146
|
+
# Unpack the EOCD and return a boolean indicating whether this header is
|
|
147
|
+
# complete without needing Zip64 extensions.
|
|
148
|
+
def unpack_e_o_c_d(buffer) # :nodoc: # rubocop:disable Naming/PredicateMethod
|
|
145
149
|
_, # END_OF_CD_SIG. We know we have this at this point.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
size_in_bytes,
|
|
151
|
-
cdir_offset,
|
|
150
|
+
@number_of_this_disk,
|
|
151
|
+
@number_of_disk_with_start_of_cdir,
|
|
152
|
+
@total_number_of_entries_in_cdir_on_this_disk,
|
|
153
|
+
@size,
|
|
154
|
+
@size_in_bytes,
|
|
155
|
+
@cdir_offset,
|
|
152
156
|
comment_length = buffer.unpack('VvvvvVVv')
|
|
153
157
|
|
|
154
|
-
@number_of_this_disk = num_disk unless num_disk == 0xFFFF
|
|
155
|
-
@number_of_disk_with_start_of_cdir = num_disk_cdir unless num_disk_cdir == 0xFFFF
|
|
156
|
-
@total_number_of_entries_in_cdir_on_this_disk = num_cdir_disk unless num_cdir_disk == 0xFFFF
|
|
157
|
-
@size = num_entries unless num_entries == 0xFFFF
|
|
158
|
-
@size_in_bytes = size_in_bytes unless size_in_bytes == 0xFFFFFFFF
|
|
159
|
-
@cdir_offset = cdir_offset unless cdir_offset == 0xFFFFFFFF
|
|
160
|
-
|
|
161
158
|
@comment = if comment_length.positive?
|
|
162
159
|
buffer.slice(STATIC_EOCD_SIZE, comment_length)
|
|
163
160
|
else
|
|
164
161
|
''
|
|
165
162
|
end
|
|
163
|
+
|
|
164
|
+
!([@number_of_this_disk, @number_of_disk_with_start_of_cdir,
|
|
165
|
+
@total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
|
|
166
|
+
@size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
def read_central_directory_entries(io) # :nodoc:
|
|
@@ -182,7 +183,7 @@ module Zip
|
|
|
182
183
|
next unless entry
|
|
183
184
|
|
|
184
185
|
offset = if entry.zip64?
|
|
185
|
-
entry.extra[
|
|
186
|
+
entry.extra[:zip64].relative_header_offset
|
|
186
187
|
else
|
|
187
188
|
entry.local_header_offset
|
|
188
189
|
end
|
|
@@ -215,30 +216,37 @@ module Zip
|
|
|
215
216
|
eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
|
|
216
217
|
raise Error, 'Zip end of central directory signature not found' unless eocd_location
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if zip64_eocd_locator
|
|
221
|
-
zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'))
|
|
219
|
+
# Parse the EOCD and return if it is complete without Zip64 extensions.
|
|
220
|
+
return if unpack_e_o_c_d(data.slice(eocd_location..-1))
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
data.slice(zip64_eocd_locator..eocd_location)
|
|
229
|
-
)
|
|
230
|
-
unless zip64_eocd_location
|
|
231
|
-
raise Error, 'Zip64 end of central directory signature not found'
|
|
232
|
-
end
|
|
222
|
+
# Need to read in the Zip64 EOCD locator and then the Zip64 EOCD.
|
|
223
|
+
zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'), eocd_location)
|
|
224
|
+
unless zip64_eocd_locator
|
|
225
|
+
raise Error, 'Zip64 end of central directory locator signature expected but not found'
|
|
226
|
+
end
|
|
233
227
|
|
|
234
|
-
|
|
235
|
-
|
|
228
|
+
# Do we already have the Zip64 EOCD in the data we've read?
|
|
229
|
+
zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'), zip64_eocd_locator)
|
|
230
|
+
|
|
231
|
+
zip64_eocd_data =
|
|
232
|
+
if zip64_eocd_location
|
|
233
|
+
# Yes.
|
|
234
|
+
data.slice(zip64_eocd_location..zip64_eocd_locator)
|
|
235
|
+
else
|
|
236
|
+
# No. Read its location from the locator and then read it in.
|
|
237
|
+
zip64_eocd_location = unpack_64_eocd_locator(
|
|
238
|
+
data.slice(zip64_eocd_locator..eocd_location)
|
|
239
|
+
)
|
|
240
|
+
unless zip64_eocd_location
|
|
241
|
+
raise Error, 'Zip64 end of central directory signature not found'
|
|
236
242
|
end
|
|
237
243
|
|
|
238
|
-
|
|
239
|
-
|
|
244
|
+
io.seek(zip64_eocd_location, IO::SEEK_SET)
|
|
245
|
+
io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
|
|
246
|
+
end
|
|
240
247
|
|
|
241
|
-
|
|
248
|
+
# Finally, unpack the Zip64 EOCD.
|
|
249
|
+
unpack_64_e_o_c_d(zip64_eocd_data)
|
|
242
250
|
end
|
|
243
251
|
|
|
244
252
|
def eocd_data(io)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
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
|
+
@password = password
|
|
49
|
+
@strength = strength
|
|
50
|
+
@bits = BITS[@strength]
|
|
51
|
+
@key_length = KEY_LENGTHS[@strength]
|
|
52
|
+
@salt_length = SALT_LENGTHS[@strength]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def header_bytesize
|
|
56
|
+
@salt_length + VERIFIER_LENGTH
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def gp_flags
|
|
60
|
+
0x0001
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class AESDecrypter < Decrypter # :nodoc:
|
|
65
|
+
include AESEncryption
|
|
66
|
+
|
|
67
|
+
def decrypt(encrypted_data)
|
|
68
|
+
@hmac.update(encrypted_data)
|
|
69
|
+
|
|
70
|
+
idx = 0
|
|
71
|
+
decrypted_data = +''
|
|
72
|
+
amount_to_read = encrypted_data.size
|
|
73
|
+
|
|
74
|
+
while amount_to_read.positive?
|
|
75
|
+
@cipher.iv = [@counter + 1].pack('Vx12')
|
|
76
|
+
begin_index = BLOCK_SIZE * idx
|
|
77
|
+
end_index = begin_index + [BLOCK_SIZE, amount_to_read].min
|
|
78
|
+
decrypted_data << @cipher.update(encrypted_data[begin_index...end_index])
|
|
79
|
+
amount_to_read -= BLOCK_SIZE
|
|
80
|
+
@counter += 1
|
|
81
|
+
idx += 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# JRuby requires finalization of the cipher. This is a bug, as noted in
|
|
85
|
+
# jruby/jruby-openssl#182 and jruby/jruby-openssl#183.
|
|
86
|
+
decrypted_data << @cipher.final if defined?(JRUBY_VERSION)
|
|
87
|
+
decrypted_data
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reset!(header)
|
|
91
|
+
raise Error, "Unsupported encryption AES-#{@bits}" unless STRENGTHS.include? @strength
|
|
92
|
+
|
|
93
|
+
salt = header[0...@salt_length]
|
|
94
|
+
pwd_verify = header[-VERIFIER_LENGTH..]
|
|
95
|
+
key_material = OpenSSL::KDF.pbkdf2_hmac(
|
|
96
|
+
@password,
|
|
97
|
+
salt: salt,
|
|
98
|
+
iterations: 1000,
|
|
99
|
+
length: (2 * @key_length) + VERIFIER_LENGTH,
|
|
100
|
+
hash: 'sha1'
|
|
101
|
+
)
|
|
102
|
+
enc_key = key_material[0...@key_length]
|
|
103
|
+
enc_hmac_key = key_material[@key_length...(2 * @key_length)]
|
|
104
|
+
enc_pwd_verify = key_material[-VERIFIER_LENGTH..]
|
|
105
|
+
|
|
106
|
+
raise Error, 'Bad password' if enc_pwd_verify != pwd_verify
|
|
107
|
+
|
|
108
|
+
@counter = 0
|
|
109
|
+
@cipher = OpenSSL::Cipher::AES.new(@bits, :CTR)
|
|
110
|
+
@cipher.decrypt
|
|
111
|
+
@cipher.key = enc_key
|
|
112
|
+
@hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def check_integrity!(io)
|
|
116
|
+
auth_code = io.read(AUTHENTICATION_CODE_LENGTH)
|
|
117
|
+
raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -4,39 +4,43 @@ module Zip
|
|
|
4
4
|
class DecryptedIo # :nodoc:all
|
|
5
5
|
CHUNK_SIZE = 32_768
|
|
6
6
|
|
|
7
|
-
def initialize(io, decrypter)
|
|
7
|
+
def initialize(io, decrypter, compressed_size)
|
|
8
8
|
@io = io
|
|
9
9
|
@decrypter = decrypter
|
|
10
|
+
@bytes_remaining = compressed_size
|
|
11
|
+
@buffer = +''
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def read(length = nil, outbuf = +'')
|
|
13
|
-
return (length.nil? || length.zero? ? '' : nil) if eof
|
|
15
|
+
return (length.nil? || length.zero? ? '' : nil) if eof?
|
|
14
16
|
|
|
15
|
-
while length.nil? || (buffer.bytesize < length)
|
|
17
|
+
while length.nil? || (@buffer.bytesize < length)
|
|
16
18
|
break if input_finished?
|
|
17
19
|
|
|
18
|
-
buffer << produce_input
|
|
20
|
+
@buffer << produce_input
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
@decrypter.check_integrity!(@io) if input_finished?
|
|
24
|
+
|
|
25
|
+
outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
private
|
|
25
29
|
|
|
26
|
-
def eof
|
|
27
|
-
buffer.empty? && input_finished?
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def buffer
|
|
31
|
-
@buffer ||= +''
|
|
30
|
+
def eof?
|
|
31
|
+
@buffer.empty? && input_finished?
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def input_finished?
|
|
35
|
-
|
|
35
|
+
!@bytes_remaining.positive?
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def produce_input
|
|
39
|
-
@
|
|
39
|
+
chunk_size = [@bytes_remaining, CHUNK_SIZE].min
|
|
40
|
+
return '' unless chunk_size.positive?
|
|
41
|
+
|
|
42
|
+
@bytes_remaining -= chunk_size
|
|
43
|
+
@decrypter.decrypt(@io.read(chunk_size))
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
46
|
end
|
|
@@ -28,16 +28,6 @@ module Zip
|
|
|
28
28
|
|
|
29
29
|
def reset!; end
|
|
30
30
|
end
|
|
31
|
-
|
|
32
|
-
class NullDecrypter < Decrypter # :nodoc:
|
|
33
|
-
include NullEncryption
|
|
34
|
-
|
|
35
|
-
def decrypt(data)
|
|
36
|
-
data
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def reset!(_header); end
|
|
40
|
-
end
|
|
41
31
|
end
|
|
42
32
|
|
|
43
33
|
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
data/lib/zip/dos_time.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Zip
|
|
|
21
21
|
def absolute_time?
|
|
22
22
|
# If absolute time is not set, we can assume it is an absolute time
|
|
23
23
|
# because times do have timezone information by default.
|
|
24
|
-
@absolute_time.nil?
|
|
24
|
+
@absolute_time.nil? || @absolute_time
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def to_binary_dos_time
|
|
@@ -36,7 +36,8 @@ module Zip
|
|
|
36
36
|
((year - 1980) << 9)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# Deprecated. Remove for version 4.
|
|
40
|
+
def dos_equals(other) # rubocop:disable Naming/PredicateMethod
|
|
40
41
|
warn 'Zip::DOSTime#dos_equals is deprecated. Use `==` instead.'
|
|
41
42
|
self == other
|
|
42
43
|
end
|