Skip to content

Commit

Permalink
Merge 207a3a9 into 8ede424
Browse files Browse the repository at this point in the history
  • Loading branch information
muz committed Aug 29, 2014
2 parents 8ede424 + 207a3a9 commit d15d576
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 8 deletions.
1 change: 1 addition & 0 deletions lib/zip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
require 'zip/pass_thru_compressor'
require 'zip/pass_thru_decompressor'
require 'zip/inflater'
require 'zip/decrypter'
require 'zip/deflater'
require 'zip/streamable_stream'
require 'zip/streamable_directory'
Expand Down
2 changes: 2 additions & 0 deletions lib/zip/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ module Zip
VERSION_NEEDED_TO_EXTRACT = 20
VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45

GP_FLAGS_DESCRIPTOR_PRESENT = 0x08

FILE_TYPE_FILE = 010
FILE_TYPE_DIR = 004
FILE_TYPE_SYMLINK = 012
Expand Down
2 changes: 2 additions & 0 deletions lib/zip/decompressor.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Zip
class Decompressor #:nodoc:all
attr_accessor :input_stream

CHUNK_SIZE = 32768
def initialize(input_stream)
super()
Expand Down
95 changes: 95 additions & 0 deletions lib/zip/decrypter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require 'openssl'
require 'stringio'

module Zip
class Decrypter < Decompressor #:nodoc:all
VERIFIER_LENGTH = 2
BLOCK_SIZE = 16
AUTHENTICATION_CODE_LENGTH = 10

attr_writer :password

def initialize(input_stream, encryption_strength, entry_size, decompressor)
super(input_stream)

@data_length = entry_size - AUTHENTICATION_CODE_LENGTH
@decompressor = decompressor
@decompressor.input_stream = StringIO.new
@encryption_strength = encryption_strength
@prepared = false
end

def sysread(number_of_bytes = nil, buf = '')
prepare_aes unless @prepared

amount_to_read = @data_length
raise RuntimeError, "Incorrect entry size given, can't proceed" if amount_to_read <= 0

counter = 1
while amount_to_read > 0
set_iv(counter)

encrypted = @input_stream.read([BLOCK_SIZE, amount_to_read].min)
# Add the decrypted data to the IO object the decompressor interacts with
@decompressor.input_stream.write(@cipher.update(encrypted))

amount_to_read -= BLOCK_SIZE
counter += 1
end

# TODO: Check Authentication value
@input_stream.read(AUTHENTICATION_CODE_LENGTH)

@decompressor.input_stream.rewind
@decompressor.sysread
end

def input_finished?
@decompressor.input_finished?
end

alias :eof :input_finished?
alias :eof? :input_finished?

private

def prepare_aes
raise RuntimeError, "No password given" if @password.nil?
n = @encryption_strength + 1

headers = {
bits: 64 * n,
key_length: 8 * n,
mac_length: 8 * n,
salt_length: 4 * n
}

raise RuntimeError, "AES-#{headers[:bits]} is not supported." unless [0x01, 0x02, 0x03].include? @encryption_strength

@cipher = OpenSSL::Cipher::AES.new(headers[:bits], :CTR)
@cipher.decrypt

salt = @input_stream.read(headers[:salt_length])
verification = @input_stream.read(VERIFIER_LENGTH)
# The first few bytes are AES setup. Ensure we don't read beyond the end of the data during sysread
@data_length -= (headers[:salt_length] + VERIFIER_LENGTH)

key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
@password,
salt,
1000,
headers[:key_length] + headers[:mac_length] + VERIFIER_LENGTH
)

raise RuntimeError, "Incorrect password" unless key[-2..-1] == verification
@cipher.key = key

@prepared = true
end

def set_iv(counter)
# Reverse engineered this value from Zip4j's AES support.
@cipher.iv = [counter].pack("Vx12")
end
end
end
34 changes: 33 additions & 1 deletion lib/zip/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Zip
class Entry
STORED = 0
DEFLATED = 8
ENCRYPTED = 99
# Language encoding flag (EFS) bit
EFS = 0b100000000000

Expand Down Expand Up @@ -141,7 +142,10 @@ def cdir_header_size #:nodoc:all
end

def next_header_offset #:nodoc:all
local_entry_offset + self.compressed_size
# FIXME. the data descriptor can be only 12 bytes long, if the signature isn't present
# I need to add code to deal with this!
# I also have no idea why I have to subtract 8 bytes for AES zips. It works, sooooo...
local_entry_offset + self.compressed_size + (@data_descriptor_present ? 16 : 0) - (@extra.member?('AES') ? 8 : 0)
end

# Extracts entry to file dest_path (defaults to @name).
Expand Down Expand Up @@ -248,6 +252,34 @@ def read_local_entry(io) #:nodoc:all
end
parse_zip64_extra(true)
@local_header_size = calculate_local_header_size

# If the "data descriptor present" bit is set in the general flags
# we need to read the uncompressed size and CRC from the data descriptor
# otherwise they'll remain as 0
@data_descriptor_present = (@gp_flags & ::Zip::GP_FLAGS_DESCRIPTOR_PRESENT != 0)
if @data_descriptor_present
pos = io.tell
# We need to seek forwards until we find the data descriptor signature
# (504B0708) or the next record's local file header signature (504B0304)
# and scan back a few
last_four = []

loop do
last_four.push(io.read(1))
last_four = last_four[-4..-1] if last_four.length > 4

case last_four
when %W{\x50 \x4B \x07 \x08}
break
when %W{\x50 \x4B \x03 \x04}
io.seek(-12,IO::SEEK_CUR )
break
end
end

@crc, @compressed_size, @size = io.read(12).unpack("VVV")
io.seek(pos)
end
end

def pack_local_entry
Expand Down
1 change: 1 addition & 0 deletions lib/zip/extra_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def local_size
require 'zip/extra_field/unix'
require 'zip/extra_field/zip64'
require 'zip/extra_field/zip64_placeholder'
require 'zip/extra_field/aes'

# Copyright (C) 2002, 2003 Thomas Sondergaard
# rubyzip is free software; you can redistribute it and/or
Expand Down
31 changes: 31 additions & 0 deletions lib/zip/extra_field/aes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Zip
# Info-ZIP Extra for AES encryption
class ExtraField::AES < ExtraField::Generic
attr_reader :data_size, :vendor_version, :vendor_id, :encryption_strength, :compression_method
HEADER_ID = "\x01\x99".force_encoding("ASCII-8BIT")
register_map

def initialize(binstr = nil)
@data_size = nil
@vendor_version = nil
@vendor_id = nil
@encryption_strength = nil
@compression_method = nil
binstr and merge(binstr)
end

def merge(binstr)
return if binstr.empty?
_, @data_size, @vendor_version, @vendor_id, @encryption_strength, @compression_method = binstr.to_s.unpack("vvva2Cv")
end

def pack_for_local
return '' unless @data_size && @vendor_version && @vendor_id && @encryption_strength && @compression_method
[0x01, 0x99, @data_size, @vendor_version, @vendor_id, @encryption_strength, @compression_method].pack("vvvvM2Cv")
end

def pack_for_c_dir
pack_for_local
end
end
end
15 changes: 14 additions & 1 deletion lib/zip/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ def initialize(file_name, create = nil, buffer = false, options = {})
@restore_times = options[:restore_times] || true
end

def password=(password)
@password = password
end

class << self
# Same as #new. If a block is passed the ZipFile object is passed
# to the block and is automatically closed afterwards just as with
Expand Down Expand Up @@ -220,7 +224,16 @@ def split(zip_file_name, segment_size = MAX_SEGMENT_SIZE, delete_zip_file = true
# the stream object is passed to the block and the stream is automatically
# closed afterwards just as with ruby's builtin File.open method.
def get_input_stream(entry, &aProc)
get_entry(entry).get_input_stream(&aProc)

if aProc
get_entry(entry).get_input_stream do |zis|
zis.password = @password if zis.respond_to? :password= # Make sure that tempfile does not call :password=
aProc.call(zis)
end
else
get_entry(entry).get_input_stream
end

end

# Returns an output stream to the specified entry. If entry is not an instance
Expand Down
34 changes: 28 additions & 6 deletions lib/zip/input_stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,39 @@ def open_entry
@current_entry
end

def get_decompressor
case
when @current_entry.nil?
def get_decompressor(compression_method = nil)
compression_method ||= @current_entry.compression_method unless @current_entry.nil?
case compression_method
when nil
::Zip::NullDecompressor
when @current_entry.compression_method == ::Zip::Entry::STORED
when ::Zip::Entry::STORED
::Zip::PassThruDecompressor.new(@archive_io, @current_entry.size)
when @current_entry.compression_method == ::Zip::Entry::DEFLATED
when ::Zip::Entry::DEFLATED
::Zip::Inflater.new(@archive_io)
when ::Zip::Entry::ENCRYPTED
if @current_entry.extra["AES"].vendor_id != "AE"
raise ZipCompressionMethodError, "The #{@current_entry.extra["AES"].vendor_id} encryption method is not supported"
end

unless [1,2].include? @current_entry.extra["AES"].vendor_version
raise ZipCompressionMethodError, "Only AES-1 and AES-2 style encryption is supported"
end

if @current_entry.extra["AES"].compression_method == ::Zip::Entry::ENCRYPTED
# This would create infinite recursion.
raise ZipCompressionMethodError, "This zip file is malformed"
end

::Zip::Decrypter.new(
@archive_io,
@current_entry.extra["AES"].encryption_strength,
@current_entry.compressed_size,
get_decompressor(@current_entry.extra["AES"].compression_method)
)

else
raise ::Zip::CompressionMethodError,
"Unsupported compression method #{@current_entry.compression_method}"
"Unsupported compression method #{compression_method}"
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/zip/ioextras/abstract_input_stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ def initialize

attr_accessor :lineno
attr_reader :pos
attr_writer :password

def read(number_of_bytes = nil, buf = '')
@decompressor.password = @password if !@password.nil? and @decompressor.respond_to? :password=
tbuf = if @output_buffer.bytesize > 0
if number_of_bytes <= @output_buffer.bytesize
@output_buffer.slice!(0, number_of_bytes)
Expand Down

0 comments on commit d15d576

Please sign in to comment.