rubyzip 3.0.2 → 3.1.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.
- checksums.yaml +4 -4
- data/Changelog.md +8 -0
- data/README.md +13 -5
- data/lib/zip/crypto/aes_encryption.rb +119 -0
- data/lib/zip/crypto/decrypted_io.rb +16 -3
- data/lib/zip/entry.rb +21 -0
- data/lib/zip/extra_field/aes.rb +45 -0
- data/lib/zip/extra_field.rb +1 -0
- data/lib/zip/input_stream.rb +14 -1
- data/lib/zip/version.rb +2 -1
- data/lib/zip.rb +1 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f572a7ce284e7e1dcdd10b0ba8e278c862957e9ac47d1b00dad5ea26db6fcba
|
4
|
+
data.tar.gz: 8afd167d9dcd0b902d79d1dc4111964c3edb75739419cd30cff9ebc36c38bdc7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e09c8b007382e0243cdb57d63560d5b6c8ef9a153c8dafc35a1c284ad41dc49c5afc284cf6175ffc311691e98dcdd81221cee8f2d1b060a665ade5de11d00c58
|
7
|
+
data.tar.gz: 306dfe485b10b421d4232c44314bd609c14004b3f0b1b8d76d7ef78234f6a34dc76eae38126a53ac34553f7538a87a807991be1d5b06b8788a81bd8eba723fdc
|
data/Changelog.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 3.1.0 (2025-09-06)
|
2
|
+
|
3
|
+
- Support AES decryption. [#579](https://github.com/rubyzip/rubyzip/pull/579) and [#645](https://github.com/rubyzip/rubyzip/pull/645)
|
4
|
+
|
5
|
+
Tooling/internal:
|
6
|
+
|
7
|
+
- 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.
|
8
|
+
|
1
9
|
# 3.0.2 (2025-08-21)
|
2
10
|
|
3
11
|
- Fix `InputStream#sysread` to handle frozen string literals. [#643](https://github.com/rubyzip/rubyzip/pull/643)
|
data/README.md
CHANGED
@@ -201,11 +201,11 @@ end
|
|
201
201
|
|
202
202
|
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
203
|
|
204
|
-
### Password Protection (
|
204
|
+
### Password Protection (experimental)
|
205
205
|
|
206
|
-
Rubyzip supports reading
|
206
|
+
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
207
|
|
208
|
-
#### Version 2.x
|
208
|
+
#### Version 2.x (ZipCrypto only)
|
209
209
|
|
210
210
|
```ruby
|
211
211
|
# Writing.
|
@@ -224,9 +224,17 @@ Zip::InputStream.open(buffer, 0, dec) do |input|
|
|
224
224
|
end
|
225
225
|
```
|
226
226
|
|
227
|
-
#### Version 3.x
|
227
|
+
#### Version 3.x (AES reading and ZipCrypto read/write)
|
228
228
|
|
229
229
|
```ruby
|
230
|
+
# Reading AES, version 3.1 and later.
|
231
|
+
dec = Zip::AESDecrypter.new('password', Zip::AESEncryption::STRENGTH_256_BIT)
|
232
|
+
Zip::InputStream.open('aes-encrypted-file.zip', decrypter: dec) do |input|
|
233
|
+
entry = input.get_next_entry
|
234
|
+
puts "Contents of '#{entry.name}':"
|
235
|
+
puts input.read
|
236
|
+
end
|
237
|
+
|
230
238
|
# Writing.
|
231
239
|
enc = Zip::TraditionalEncrypter.new('password')
|
232
240
|
buffer = Zip::OutputStream.write_buffer(encrypter: enc) do |output|
|
@@ -243,7 +251,7 @@ Zip::InputStream.open(buffer, decrypter: dec) do |input|
|
|
243
251
|
end
|
244
252
|
```
|
245
253
|
|
246
|
-
_This is an
|
254
|
+
_This is an evolving feature and the interface for encryption may change in future versions._
|
247
255
|
|
248
256
|
## Known issues
|
249
257
|
|
@@ -0,0 +1,119 @@
|
|
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(auth_code)
|
116
|
+
raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -4,9 +4,11 @@ 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
|
+
@offset = io.tell
|
11
|
+
@compressed_size = compressed_size
|
10
12
|
end
|
11
13
|
|
12
14
|
def read(length = nil, outbuf = +'')
|
@@ -18,6 +20,10 @@ module Zip
|
|
18
20
|
buffer << produce_input
|
19
21
|
end
|
20
22
|
|
23
|
+
if @decrypter.kind_of?(::Zip::AESDecrypter) && input_finished?
|
24
|
+
@decrypter.check_integrity(@io.read(::Zip::AESEncryption::AUTHENTICATION_CODE_LENGTH))
|
25
|
+
end
|
26
|
+
|
21
27
|
outbuf.replace(buffer.slice!(0...(length || buffer.bytesize)))
|
22
28
|
end
|
23
29
|
|
@@ -31,12 +37,19 @@ module Zip
|
|
31
37
|
@buffer ||= +''
|
32
38
|
end
|
33
39
|
|
40
|
+
def pos
|
41
|
+
@io.tell - @offset
|
42
|
+
end
|
43
|
+
|
34
44
|
def input_finished?
|
35
|
-
@io.eof
|
45
|
+
@io.eof || pos >= @compressed_size
|
36
46
|
end
|
37
47
|
|
38
48
|
def produce_input
|
39
|
-
@
|
49
|
+
chunk_size = [CHUNK_SIZE, @compressed_size - pos].min
|
50
|
+
return '' unless chunk_size.positive?
|
51
|
+
|
52
|
+
@decrypter.decrypt(@io.read(chunk_size))
|
40
53
|
end
|
41
54
|
end
|
42
55
|
end
|
data/lib/zip/entry.rb
CHANGED
@@ -203,6 +203,11 @@ module Zip
|
|
203
203
|
!@extra['Zip64'].nil?
|
204
204
|
end
|
205
205
|
|
206
|
+
# Is this entry encrypted with AES encryption?
|
207
|
+
def aes?
|
208
|
+
!@extra['AES'].nil?
|
209
|
+
end
|
210
|
+
|
206
211
|
def file_type_is?(type) # :nodoc:
|
207
212
|
ftype == type
|
208
213
|
end
|
@@ -382,6 +387,7 @@ module Zip
|
|
382
387
|
|
383
388
|
read_extra_field(extra, local: true)
|
384
389
|
parse_zip64_extra(true)
|
390
|
+
parse_aes_extra
|
385
391
|
@local_header_size = calculate_local_header_size
|
386
392
|
end
|
387
393
|
|
@@ -511,6 +517,7 @@ module Zip
|
|
511
517
|
check_c_dir_entry_comment_size
|
512
518
|
set_ftype_from_c_dir_entry
|
513
519
|
parse_zip64_extra(false)
|
520
|
+
parse_aes_extra
|
514
521
|
end
|
515
522
|
|
516
523
|
def file_stat(path) # :nodoc:
|
@@ -800,6 +807,20 @@ module Zip
|
|
800
807
|
end
|
801
808
|
end
|
802
809
|
|
810
|
+
def parse_aes_extra # :nodoc:
|
811
|
+
return unless aes?
|
812
|
+
|
813
|
+
if @extra['AES'].vendor_id != 'AE'
|
814
|
+
raise Error, "Unsupported encryption method #{@extra['AES'].vendor_id}"
|
815
|
+
end
|
816
|
+
|
817
|
+
unless ::Zip::AESEncryption::VERSIONS.include? @extra['AES'].vendor_version
|
818
|
+
raise Error, "Unsupported encryption style #{@extra['AES'].vendor_version}"
|
819
|
+
end
|
820
|
+
|
821
|
+
@compression_method = @extra['AES'].compression_method if ftype != :directory
|
822
|
+
end
|
823
|
+
|
803
824
|
# For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
|
804
825
|
# indicate compression level. This seems to be mainly cosmetic but they are
|
805
826
|
# generally set by other tools - including in docx files. It is these flags
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zip
|
4
|
+
# Info-ZIP Extra for AES encryption
|
5
|
+
class ExtraField::AES < ExtraField::Generic # :nodoc:
|
6
|
+
attr_reader :vendor_version, :vendor_id, :encryption_strength, :compression_method
|
7
|
+
|
8
|
+
HEADER_ID = [0x9901].pack('v')
|
9
|
+
register_map
|
10
|
+
|
11
|
+
def initialize(binstr = nil)
|
12
|
+
@vendor_version = nil
|
13
|
+
@vendor_id = nil
|
14
|
+
@encryption_strength = nil
|
15
|
+
@compression_method = nil
|
16
|
+
binstr && merge(binstr)
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
@vendor_version == other.vendor_version &&
|
21
|
+
@vendor_id == other.vendor_id &&
|
22
|
+
@encryption_strength == other.encryption_strength &&
|
23
|
+
@compression_method == other.compression_method
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge(binstr)
|
27
|
+
return if binstr.empty?
|
28
|
+
|
29
|
+
size, content = initial_parse(binstr)
|
30
|
+
(size && content) || return
|
31
|
+
|
32
|
+
@vendor_version, @vendor_id,
|
33
|
+
@encryption_strength, @compression_method = content.unpack('va2Cv')
|
34
|
+
end
|
35
|
+
|
36
|
+
def pack_for_local
|
37
|
+
[@vendor_version, @vendor_id,
|
38
|
+
@encryption_strength, @compression_method].pack('va2Cv')
|
39
|
+
end
|
40
|
+
|
41
|
+
def pack_for_c_dir
|
42
|
+
pack_for_local
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/zip/extra_field.rb
CHANGED
@@ -91,6 +91,7 @@ require 'zip/extra_field/old_unix'
|
|
91
91
|
require 'zip/extra_field/unix'
|
92
92
|
require 'zip/extra_field/zip64'
|
93
93
|
require 'zip/extra_field/ntfs'
|
94
|
+
require 'zip/extra_field/aes'
|
94
95
|
|
95
96
|
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
96
97
|
# rubyzip is free software; you can redistribute it and/or
|
data/lib/zip/input_stream.rb
CHANGED
@@ -154,7 +154,20 @@ module Zip
|
|
154
154
|
header = @archive_io.read(@decrypter.header_bytesize)
|
155
155
|
@decrypter.reset!(header)
|
156
156
|
|
157
|
-
|
157
|
+
compressed_size =
|
158
|
+
if @current_entry.incomplete? && @current_entry.crc == 0 &&
|
159
|
+
@current_entry.compressed_size == 0 && @complete_entry
|
160
|
+
@complete_entry.compressed_size
|
161
|
+
else
|
162
|
+
@current_entry.compressed_size
|
163
|
+
end
|
164
|
+
|
165
|
+
if @decrypter.kind_of?(::Zip::AESDecrypter)
|
166
|
+
compressed_size -= @decrypter.header_bytesize
|
167
|
+
compressed_size -= ::Zip::AESEncryption::AUTHENTICATION_CODE_LENGTH
|
168
|
+
end
|
169
|
+
|
170
|
+
::Zip::DecryptedIo.new(@archive_io, @decrypter, compressed_size)
|
158
171
|
end
|
159
172
|
|
160
173
|
def get_decompressor # :nodoc:
|
data/lib/zip/version.rb
CHANGED
data/lib/zip.rb
CHANGED
@@ -30,6 +30,7 @@ require 'zip/crypto/decrypted_io'
|
|
30
30
|
require 'zip/crypto/encryption'
|
31
31
|
require 'zip/crypto/null_encryption'
|
32
32
|
require 'zip/crypto/traditional_encryption'
|
33
|
+
require 'zip/crypto/aes_encryption'
|
33
34
|
require 'zip/inflater'
|
34
35
|
require 'zip/deflater'
|
35
36
|
require 'zip/streamable_stream'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubyzip
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Haines
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2025-
|
13
|
+
date: 2025-09-06 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: minitest
|
@@ -141,6 +141,7 @@ files:
|
|
141
141
|
- lib/zip/central_directory.rb
|
142
142
|
- lib/zip/compressor.rb
|
143
143
|
- lib/zip/constants.rb
|
144
|
+
- lib/zip/crypto/aes_encryption.rb
|
144
145
|
- lib/zip/crypto/decrypted_io.rb
|
145
146
|
- lib/zip/crypto/encryption.rb
|
146
147
|
- lib/zip/crypto/null_encryption.rb
|
@@ -153,6 +154,7 @@ files:
|
|
153
154
|
- lib/zip/entry_set.rb
|
154
155
|
- lib/zip/errors.rb
|
155
156
|
- lib/zip/extra_field.rb
|
157
|
+
- lib/zip/extra_field/aes.rb
|
156
158
|
- lib/zip/extra_field/generic.rb
|
157
159
|
- lib/zip/extra_field/ntfs.rb
|
158
160
|
- lib/zip/extra_field/old_unix.rb
|
@@ -195,9 +197,9 @@ licenses:
|
|
195
197
|
- BSD-2-Clause
|
196
198
|
metadata:
|
197
199
|
bug_tracker_uri: https://github.com/rubyzip/rubyzip/issues
|
198
|
-
changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.0
|
199
|
-
documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.0
|
200
|
-
source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.0
|
200
|
+
changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.1.0/Changelog.md
|
201
|
+
documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.1.0
|
202
|
+
source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.1.0
|
201
203
|
wiki_uri: https://github.com/rubyzip/rubyzip/wiki
|
202
204
|
rubygems_mfa_required: 'true'
|
203
205
|
post_install_message:
|
@@ -215,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
215
217
|
- !ruby/object:Gem::Version
|
216
218
|
version: '0'
|
217
219
|
requirements: []
|
218
|
-
rubygems_version: 3.4.
|
220
|
+
rubygems_version: 3.4.19
|
219
221
|
signing_key:
|
220
222
|
specification_version: 4
|
221
223
|
summary: rubyzip is a ruby module for reading and writing zip files
|