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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4afc6447f838d0d2edc82bb44ab842a5c45f50bd47d453aae1a7b487c9dcbee
4
- data.tar.gz: 776833d664351c8e0323826263d16b2b11486fee80a7ceda1a8385d05dd2a38e
3
+ metadata.gz: 6f572a7ce284e7e1dcdd10b0ba8e278c862957e9ac47d1b00dad5ea26db6fcba
4
+ data.tar.gz: 8afd167d9dcd0b902d79d1dc4111964c3edb75739419cd30cff9ebc36c38bdc7
5
5
  SHA512:
6
- metadata.gz: b52059d2cfba5c05959bb347383d4bfe79c8f3c0f945cba64362462267c750f0e16e13f63fbf272b2cf0935173678b876164c3993ddae85fb89dec726afea940
7
- data.tar.gz: 83a932fbadf6fbafa0a1e64434f0b2a2b7f58fb56d9015350eae217430f9afaf5492254a56de9be124bdf9676a8a5aa373515c996ce2f74966aceb87b72472d1
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 (Experimental)
204
+ ### Password Protection (experimental)
205
205
 
206
- Rubyzip supports reading/writing zip files with traditional zip encryption (a.k.a. "ZipCrypto"). AES encryption is not yet supported. It can be used with buffer streams, e.g.:
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 experimental feature and the interface for encryption may change in future versions._
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
- @decrypter.decrypt(@io.read(CHUNK_SIZE))
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
@@ -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
@@ -154,7 +154,20 @@ module Zip
154
154
  header = @archive_io.read(@decrypter.header_bytesize)
155
155
  @decrypter.reset!(header)
156
156
 
157
- ::Zip::DecryptedIo.new(@archive_io, @decrypter)
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zip
4
- VERSION = '3.0.2'
4
+ # The version of the Rubyzip library.
5
+ VERSION = '3.1.0'
5
6
  end
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.2
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-08-21 00:00:00.000000000 Z
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.2/Changelog.md
199
- documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.0.2
200
- source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.0.2
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.1
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