From c70e5836c7cbbf1ebc3e515ebd73450cf1eac2f7 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Wed, 31 Dec 2014 01:03:17 +0900 Subject: [PATCH 01/10] support traditional encryption --- lib/zip.rb | 3 ++ lib/zip/deflater.rb | 10 ++-- lib/zip/encryption.rb | 21 ++++++++ lib/zip/inflater.rb | 7 +-- lib/zip/input_stream.rb | 12 ++++- lib/zip/null_encryption.rb | 37 +++++++++++++ lib/zip/output_stream.rb | 9 +++- lib/zip/traditional_encryption.rb | 90 +++++++++++++++++++++++++++++++ 8 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 lib/zip/encryption.rb create mode 100644 lib/zip/null_encryption.rb create mode 100644 lib/zip/traditional_encryption.rb diff --git a/lib/zip.rb b/lib/zip.rb index 6ee5814e..7aba211d 100644 --- a/lib/zip.rb +++ b/lib/zip.rb @@ -21,6 +21,9 @@ require 'zip/null_input_stream' require 'zip/pass_thru_compressor' require 'zip/pass_thru_decompressor' +require 'zip/encryption' +require 'zip/null_encryption' +require 'zip/traditional_encryption' require 'zip/inflater' require 'zip/deflater' require 'zip/streamable_stream' diff --git a/lib/zip/deflater.rb b/lib/zip/deflater.rb index 84a8c0bf..82da54ac 100755 --- a/lib/zip/deflater.rb +++ b/lib/zip/deflater.rb @@ -1,23 +1,25 @@ module Zip class Deflater < Compressor #:nodoc:all - def initialize(output_stream, level = Zip.default_compression) + def initialize(output_stream, level = Zip.default_compression, encrypter = NullEncrypter.new) super() @output_stream = output_stream @zlib_deflater = ::Zlib::Deflate.new(level, -::Zlib::MAX_WBITS) @size = 0 @crc = ::Zlib.crc32 + @encrypter = encrypter + @output_stream << @encrypter.header(@crc) end def << (data) val = data.to_s @crc = Zlib::crc32(val, @crc) - @size += val.bytesize - @output_stream << @zlib_deflater.deflate(data) + @size += val.bytesize + @encrypter.header_bytesize + @output_stream << @encrypter.encrypt(@zlib_deflater.deflate(data)) end def finish - @output_stream << @zlib_deflater.finish until @zlib_deflater.finished? + @output_stream << @encrypter.encrypt(@zlib_deflater.finish) until @zlib_deflater.finished? end attr_reader :size, :crc diff --git a/lib/zip/encryption.rb b/lib/zip/encryption.rb new file mode 100644 index 00000000..26b18215 --- /dev/null +++ b/lib/zip/encryption.rb @@ -0,0 +1,21 @@ +module Zip + class Encrypter #:nodoc:all + def self.build(password) + if password.nil? or password.empty? + NullEncrypter.new + else + TraditionalEncrypter.new(password) + end + end + end + + class Decrypter + def self.build(password) + if password.nil? or password.empty? + NullDecrypter.new + else + TraditionalDecrypter.new(password) + end + end + end +end diff --git a/lib/zip/inflater.rb b/lib/zip/inflater.rb index 615a0c28..ca7aeb50 100755 --- a/lib/zip/inflater.rb +++ b/lib/zip/inflater.rb @@ -1,10 +1,11 @@ module Zip class Inflater < Decompressor #:nodoc:all - def initialize(input_stream) - super + def initialize(input_stream, decrypter = NullDecrypter.new) + super(input_stream) @zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS) @output_buffer = '' @has_returned_empty_string = false + @decrypter = decrypter end def sysread(number_of_bytes = nil, buf = '') @@ -40,7 +41,7 @@ def input_finished? def internal_produce_input(buf = '') retried = 0 begin - @zlib_inflater.inflate(@input_stream.read(Decompressor::CHUNK_SIZE, buf)) + @zlib_inflater.inflate(@decrypter.decrypt(@input_stream.read(Decompressor::CHUNK_SIZE, buf))) rescue Zlib::BufError raise if retried >= 5 # how many times should we retry? retried += 1 diff --git a/lib/zip/input_stream.rb b/lib/zip/input_stream.rb index 9ca039b8..2101967b 100755 --- a/lib/zip/input_stream.rb +++ b/lib/zip/input_stream.rb @@ -52,12 +52,17 @@ def initialize(context, offset = 0) @archive_io = get_io(context, offset) @decompressor = ::Zip::NullDecompressor @current_entry = nil + set_password(nil) end def close @archive_io.close end + def set_password(password) + @decrypter = ::Zip::Decrypter.build(password) + end + # Returns a Entry object. It is necessary to call this # method on a newly created InputStream before reading from # the first entry in the archive. Returns nil when there are @@ -124,6 +129,9 @@ def get_io(io_or_file, offset = 0) def open_entry @current_entry = ::Zip::Entry.read_local_entry(@archive_io) + if @current_entry and @current_entry.gp_flags & 1 == 1 and @decrypter.is_a? NullEncrypter + raise Error, 'password required to decode zip file' + end @decompressor = get_decompressor flush @current_entry @@ -136,7 +144,9 @@ def get_decompressor when @current_entry.compression_method == ::Zip::Entry::STORED ::Zip::PassThruDecompressor.new(@archive_io, @current_entry.size) when @current_entry.compression_method == ::Zip::Entry::DEFLATED - ::Zip::Inflater.new(@archive_io) + header = @archive_io.read(@decrypter.header_bytesize) + @decrypter.reset!(header) + ::Zip::Inflater.new(@archive_io, @decrypter) else raise ::Zip::CompressionMethodError, "Unsupported compression method #{@current_entry.compression_method}" diff --git a/lib/zip/null_encryption.rb b/lib/zip/null_encryption.rb new file mode 100644 index 00000000..ee92fa71 --- /dev/null +++ b/lib/zip/null_encryption.rb @@ -0,0 +1,37 @@ +module Zip + module NullEncryption + def header_bytesize + 0 + end + + def gp_flags + 0 + end + end + + class NullEncrypter + include NullEncryption + + def header(crc32) + '' + end + + def encrypt(data) + data + end + + def reset! + end + end + + class NullDecrypter + include NullEncryption + + def decrypt(data) + data + end + + def reset!(header) + end + end +end diff --git a/lib/zip/output_stream.rb b/lib/zip/output_stream.rb index a2b98ff3..9b6d76b7 100755 --- a/lib/zip/output_stream.rb +++ b/lib/zip/output_stream.rb @@ -40,6 +40,7 @@ def initialize(file_name, stream=false) @closed = false @current_entry = nil @comment = nil + set_password(nil) end # Same as #initialize but if a block is passed the opened @@ -118,6 +119,10 @@ def copy_raw_entry(entry) @current_entry = nil end + def set_password(password) + @encrypter = ::Zip::Encrypter.build(password) + end + private def finalize_current_entry @@ -126,6 +131,7 @@ def finalize_current_entry @current_entry.compressed_size = @output_stream.tell - @current_entry.local_header_offset - @current_entry.calculate_local_header_size @current_entry.size = @compressor.size @current_entry.crc = @compressor.crc + @current_entry.gp_flags |= @encrypter.gp_flags @current_entry = nil @compressor = ::Zip::NullCompressor.instance end @@ -134,13 +140,14 @@ def init_next_entry(entry, level = Zip.default_compression) finalize_current_entry @entry_set << entry entry.write_local_entry(@output_stream) + @encrypter.reset! @compressor = get_compressor(entry, level) end def get_compressor(entry, level) case entry.compression_method when Entry::DEFLATED then - ::Zip::Deflater.new(@output_stream, level) + ::Zip::Deflater.new(@output_stream, level, @encrypter) when Entry::STORED then ::Zip::PassThruCompressor.new(@output_stream) else diff --git a/lib/zip/traditional_encryption.rb b/lib/zip/traditional_encryption.rb new file mode 100644 index 00000000..91328697 --- /dev/null +++ b/lib/zip/traditional_encryption.rb @@ -0,0 +1,90 @@ +module Zip + module TraditionalEncryption + def initialize(password) + @password = password + reset_keys! + end + + def header_bytesize + 12 + end + + def gp_flags + 1 + end + + protected + + def reset_keys! + @key0 = 0x12345678 + @key1 = 0x23456789 + @key2 = 0x34567890 + @password.each_byte do |byte| + update_keys(byte.chr) + end + end + + def update_keys(n) + @key0 = ~Zlib.crc32(n, ~@key0) + @key1 = ((@key1 + (@key0 & 0xff)) * 134775813 + 1) & 0xffffffff + @key2 = ~Zlib.crc32((@key1 >> 24).chr, ~@key2) + end + + def decrypt_byte + temp = (@key2 & 0xffff) | 2 + ((temp * (temp ^ 1)) >> 8) & 0xff + end + end + + class TraditionalEncrypter < Encrypter + include TraditionalEncryption + + def header(crc32) + [].tap do |header| + (header_bytesize - 1).times do + header << rand(0..255) + end + header << (crc32 >> 24) + end.map{|x| encode x}.pack("C*") + end + + def encrypt(data) + data.unpack("C*").map{|x| encode x}.pack("C*") + end + + def reset! + reset_keys! + end + + private + + def encode(n) + t = decrypt_byte + update_keys(n.chr) + t ^ n + end + end + + class TraditionalDecrypter < Decrypter + include TraditionalEncryption + + def decrypt(data) + data.unpack("C*").map{|x| decode x}.pack("C*") + end + + def reset!(header) + reset_keys! + header.each_byte do |x| + decode x + end + end + + private + + def decode(n) + n ^= decrypt_byte + update_keys(n.chr) + n + end + end +end From 23f2ead7ee2b999a7fd8af75eaf7be144f82706d Mon Sep 17 00:00:00 2001 From: Johnny Shields Date: Sun, 4 Jan 2015 03:36:53 +0900 Subject: [PATCH 02/10] - Move encryption related files to /lib/zip/crypto/ dir - NullEncrypter and NullDecrypter should inherit Encrypter and Decrypter respectively - Add copyright notice to bottom of added files for consistency --- lib/zip.rb | 6 +++--- lib/zip/{ => crypto}/encryption.rb | 4 ++++ lib/zip/{ => crypto}/null_encryption.rb | 8 ++++++-- lib/zip/{ => crypto}/traditional_encryption.rb | 4 ++++ 4 files changed, 17 insertions(+), 5 deletions(-) rename lib/zip/{ => crypto}/encryption.rb (72%) rename lib/zip/{ => crypto}/null_encryption.rb (62%) rename lib/zip/{ => crypto}/traditional_encryption.rb (91%) diff --git a/lib/zip.rb b/lib/zip.rb index 7aba211d..e6ebad33 100644 --- a/lib/zip.rb +++ b/lib/zip.rb @@ -21,9 +21,9 @@ require 'zip/null_input_stream' require 'zip/pass_thru_compressor' require 'zip/pass_thru_decompressor' -require 'zip/encryption' -require 'zip/null_encryption' -require 'zip/traditional_encryption' +require 'zip/crypto/encryption' +require 'zip/crypto/null_encryption' +require 'zip/crypto/traditional_encryption' require 'zip/inflater' require 'zip/deflater' require 'zip/streamable_stream' diff --git a/lib/zip/encryption.rb b/lib/zip/crypto/encryption.rb similarity index 72% rename from lib/zip/encryption.rb rename to lib/zip/crypto/encryption.rb index 26b18215..37b4e710 100644 --- a/lib/zip/encryption.rb +++ b/lib/zip/crypto/encryption.rb @@ -19,3 +19,7 @@ def self.build(password) end end end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/lib/zip/null_encryption.rb b/lib/zip/crypto/null_encryption.rb similarity index 62% rename from lib/zip/null_encryption.rb rename to lib/zip/crypto/null_encryption.rb index ee92fa71..37e03ede 100644 --- a/lib/zip/null_encryption.rb +++ b/lib/zip/crypto/null_encryption.rb @@ -9,7 +9,7 @@ def gp_flags end end - class NullEncrypter + class NullEncrypter < Encrypter include NullEncryption def header(crc32) @@ -24,7 +24,7 @@ def reset! end end - class NullDecrypter + class NullDecrypter < Decrypter include NullEncryption def decrypt(data) @@ -35,3 +35,7 @@ def reset!(header) end end end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/lib/zip/traditional_encryption.rb b/lib/zip/crypto/traditional_encryption.rb similarity index 91% rename from lib/zip/traditional_encryption.rb rename to lib/zip/crypto/traditional_encryption.rb index 91328697..aaed4d53 100644 --- a/lib/zip/traditional_encryption.rb +++ b/lib/zip/crypto/traditional_encryption.rb @@ -88,3 +88,7 @@ def decode(n) end end end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. From c2ecafe770ffe99e22a9544801bd896b99cfc986 Mon Sep 17 00:00:00 2001 From: Johnny Shields Date: Sun, 4 Jan 2015 04:03:46 +0900 Subject: [PATCH 03/10] Change method interfaces to allow encrypter/decrypter to be passed into stream methods --- lib/zip/crypto/encryption.rb | 14 -------------- lib/zip/input_stream.rb | 12 ++++-------- lib/zip/output_stream.rb | 16 ++++++---------- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/lib/zip/crypto/encryption.rb b/lib/zip/crypto/encryption.rb index 37b4e710..4351be1c 100644 --- a/lib/zip/crypto/encryption.rb +++ b/lib/zip/crypto/encryption.rb @@ -1,22 +1,8 @@ module Zip class Encrypter #:nodoc:all - def self.build(password) - if password.nil? or password.empty? - NullEncrypter.new - else - TraditionalEncrypter.new(password) - end - end end class Decrypter - def self.build(password) - if password.nil? or password.empty? - NullDecrypter.new - else - TraditionalDecrypter.new(password) - end - end end end diff --git a/lib/zip/input_stream.rb b/lib/zip/input_stream.rb index 2101967b..dafc3382 100755 --- a/lib/zip/input_stream.rb +++ b/lib/zip/input_stream.rb @@ -47,22 +47,18 @@ class InputStream # # @param context [String||IO||StringIO] file path or IO/StringIO object # @param offset [Integer] offset in the IO/StringIO - def initialize(context, offset = 0) + def initialize(context, offset = 0, decrypter = nil) super() @archive_io = get_io(context, offset) @decompressor = ::Zip::NullDecompressor + @decrypter = decrypter || ::Zip::NullDecrypter.new @current_entry = nil - set_password(nil) end def close @archive_io.close end - def set_password(password) - @decrypter = ::Zip::Decrypter.build(password) - end - # Returns a Entry object. It is necessary to call this # method on a newly created InputStream before reading from # the first entry in the archive. Returns nil when there are @@ -96,8 +92,8 @@ class << self # Same as #initialize but if a block is passed the opened # stream is passed to the block and closed when the block # returns. - def open(filename_or_io, offset = 0) - zio = self.new(filename_or_io, offset) + def open(filename_or_io, offset = 0, decrypter = nil) + zio = self.new(filename_or_io, offset, decrypter) return zio unless block_given? begin yield zio diff --git a/lib/zip/output_stream.rb b/lib/zip/output_stream.rb index 9b6d76b7..f28e5759 100755 --- a/lib/zip/output_stream.rb +++ b/lib/zip/output_stream.rb @@ -24,7 +24,7 @@ class OutputStream # Opens the indicated zip file. If a file with that name already # exists it will be overwritten. - def initialize(file_name, stream=false) + def initialize(file_name, stream=false, encrypter=nil) super() @file_name = file_name @output_stream = if stream @@ -37,27 +37,27 @@ def initialize(file_name, stream=false) end @entry_set = ::Zip::EntrySet.new @compressor = ::Zip::NullCompressor.instance + @encrypter = encrypter || ::Zip::NullEncrypter.new @closed = false @current_entry = nil @comment = nil - set_password(nil) end # Same as #initialize but if a block is passed the opened # stream is passed to the block and closed when the block # returns. class << self - def open(file_name) + def open(file_name, encrypter = nil) return new(file_name) unless block_given? - zos = new(file_name) + zos = new(file_name, false, encrypter) yield zos ensure zos.close if zos end # Same as #open but writes to a filestream instead - def write_buffer(io = ::StringIO.new('')) - zos = new(io, true) + def write_buffer(io = ::StringIO.new(''), encrypter = nil) + zos = new(io, true, encrypter) yield zos zos.close_buffer end @@ -119,10 +119,6 @@ def copy_raw_entry(entry) @current_entry = nil end - def set_password(password) - @encrypter = ::Zip::Encrypter.build(password) - end - private def finalize_current_entry From fd26052525455c00084f497024f74cf9b2fbabe4 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Sun, 4 Jan 2015 22:49:58 +0900 Subject: [PATCH 04/10] fix to encryption bug --- lib/zip/deflater.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/zip/deflater.rb b/lib/zip/deflater.rb index 82da54ac..d4340589 100755 --- a/lib/zip/deflater.rb +++ b/lib/zip/deflater.rb @@ -5,20 +5,22 @@ def initialize(output_stream, level = Zip.default_compression, encrypter = NullE super() @output_stream = output_stream @zlib_deflater = ::Zlib::Deflate.new(level, -::Zlib::MAX_WBITS) - @size = 0 + @size = encrypter.header_bytesize @crc = ::Zlib.crc32 @encrypter = encrypter - @output_stream << @encrypter.header(@crc) + @buffer_stream = ::StringIO.new('') end def << (data) val = data.to_s @crc = Zlib::crc32(val, @crc) - @size += val.bytesize + @encrypter.header_bytesize - @output_stream << @encrypter.encrypt(@zlib_deflater.deflate(data)) + @size += val.bytesize + @buffer_stream << @zlib_deflater.deflate(data) end def finish + @output_stream << @encrypter.header(@crc) + @output_stream << @encrypter.encrypt(@buffer_stream.string) @output_stream << @encrypter.encrypt(@zlib_deflater.finish) until @zlib_deflater.finished? end From 87d1a5a775496bf5ca4ea1b5540077ffbd3cc474 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Mon, 5 Jan 2015 23:20:48 +0900 Subject: [PATCH 05/10] added test cases for encryption support --- lib/zip/crypto/traditional_encryption.rb | 2 +- test/crypto/null_encryption_test.rb | 55 +++++++++++++++ test/crypto/traditional_encryption_test.rb | 79 ++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 test/crypto/null_encryption_test.rb create mode 100644 test/crypto/traditional_encryption_test.rb diff --git a/lib/zip/crypto/traditional_encryption.rb b/lib/zip/crypto/traditional_encryption.rb index aaed4d53..163bf5b6 100644 --- a/lib/zip/crypto/traditional_encryption.rb +++ b/lib/zip/crypto/traditional_encryption.rb @@ -42,7 +42,7 @@ class TraditionalEncrypter < Encrypter def header(crc32) [].tap do |header| (header_bytesize - 1).times do - header << rand(0..255) + header << Random.rand(0..255) end header << (crc32 >> 24) end.map{|x| encode x}.pack("C*") diff --git a/test/crypto/null_encryption_test.rb b/test/crypto/null_encryption_test.rb new file mode 100644 index 00000000..fa54b025 --- /dev/null +++ b/test/crypto/null_encryption_test.rb @@ -0,0 +1,55 @@ +require 'test_helper' + +class NullEncrypterTest < MiniTest::Test + def setup + @encrypter = ::Zip::NullEncrypter.new + end + + def test_header_bytesize + assert_equal 0, @encrypter.header_bytesize + end + + def test_gp_flags + assert_equal 0, @encrypter.gp_flags + end + + def test_header + [nil, '', 'a' * 10, 0xffffffff].each do |arg| + assert_empty @encrypter.header(arg) + end + end + + def test_encrypt + [nil, '', 'a' * 10, 0xffffffff].each do |data| + assert_equal data, @encrypter.encrypt(data) + end + end + + def test_reset! + assert_respond_to @encrypter, :reset! + end +end + +class NullDecrypterTest < MiniTest::Test + def setup + @decrypter = ::Zip::NullDecrypter.new + end + + def test_header_bytesize + assert_equal 0, @decrypter.header_bytesize + end + + def test_gp_flags + assert_equal 0, @decrypter.gp_flags + end + + def test_decrypt + [nil, '', 'a' * 10, 0xffffffff].each do |data| + assert_equal data, @decrypter.decrypt(data) + end + end + + def test_reset! + assert_respond_to @decrypter, :reset! + end +end diff --git a/test/crypto/traditional_encryption_test.rb b/test/crypto/traditional_encryption_test.rb new file mode 100644 index 00000000..fb1e4ff1 --- /dev/null +++ b/test/crypto/traditional_encryption_test.rb @@ -0,0 +1,79 @@ +require 'test_helper' + +class TraditionalEncrypterTest < MiniTest::Test + def setup + @encrypter = ::Zip::TraditionalEncrypter.new('password') + end + + def test_header_bytesize + assert_equal 12, @encrypter.header_bytesize + end + + def test_gp_flags + assert_equal 1, @encrypter.gp_flags + end + + def test_header + @encrypter.reset! + exepected = [239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*") + Random.stub(:rand, 1) do + assert_equal exepected, @encrypter.header(0xffffffff) + end + end + + def test_encrypt + @encrypter.reset! + Random.stub(:rand, 1) { @encrypter.header(0xffffffff) } + assert_raises(NoMethodError) { @encrypter.encrypt(nil) } + assert_raises(NoMethodError) { @encrypter.encrypt(1) } + assert_equal '', @encrypter.encrypt('') + assert_equal [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].pack("C*"), @encrypter.encrypt('a' * 10) + end + + def test_reset! + @encrypter.reset! + Random.stub(:rand, 1) { @encrypter.header(0xffffffff) } + [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + assert_equal c, @encrypter.encrypt('a') + end + assert_equal 134.chr, @encrypter.encrypt('a') + @encrypter.reset! + Random.stub(:rand, 1) { @encrypter.header(0xffffffff) } + [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + assert_equal c, @encrypter.encrypt('a') + end + end +end + +class TraditionalDecrypterTest < MiniTest::Test + def setup + @decrypter = ::Zip::TraditionalDecrypter.new('password') + end + + def test_header_bytesize + assert_equal 12, @decrypter.header_bytesize + end + + def test_gp_flags + assert_equal 1, @decrypter.gp_flags + end + + def test_decrypt + @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*")) + [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + assert_equal 'a', @decrypter.decrypt(c) + end + end + + def test_reset! + @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*")) + [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + assert_equal 'a', @decrypter.decrypt(c) + end + assert_equal 229.chr, @decrypter.decrypt(2.chr) + @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*")) + [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + assert_equal 'a', @decrypter.decrypt(c) + end + end +end From 744244d4b360f6aaa2759b5c40018334b716a518 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Tue, 6 Jan 2015 00:50:22 +0900 Subject: [PATCH 06/10] use guard for testing --- Guardfile | 6 ++++++ rubyzip.gemspec | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 Guardfile diff --git a/Guardfile b/Guardfile new file mode 100644 index 00000000..1508e4c9 --- /dev/null +++ b/Guardfile @@ -0,0 +1,6 @@ +guard :minitest do + # with Minitest::Unit + watch(%r{^test/(.*)\/?(.*)_test\.rb$}) + watch(%r{^lib/zip/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" } + watch(%r{^test/test_helper\.rb$}) { 'test' } +end diff --git a/rubyzip.gemspec b/rubyzip.gemspec index 3b575044..22344e84 100644 --- a/rubyzip.gemspec +++ b/rubyzip.gemspec @@ -20,4 +20,6 @@ spec = Gem::Specification.new do |s| s.add_development_dependency 'pry', '~> 0.10' s.add_development_dependency 'minitest', '~> 5.4' s.add_development_dependency 'coveralls', '~> 0.7' + s.add_development_dependency 'guard' + s.add_development_dependency 'guard-minitest' end From bd5a7e4081fffbc47a6dde32c7491fa8f1f88d02 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Wed, 7 Jan 2015 10:45:01 +0900 Subject: [PATCH 07/10] fix to file size with encrption --- lib/zip/deflater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zip/deflater.rb b/lib/zip/deflater.rb index d4340589..f304da77 100755 --- a/lib/zip/deflater.rb +++ b/lib/zip/deflater.rb @@ -5,7 +5,7 @@ def initialize(output_stream, level = Zip.default_compression, encrypter = NullE super() @output_stream = output_stream @zlib_deflater = ::Zlib::Deflate.new(level, -::Zlib::MAX_WBITS) - @size = encrypter.header_bytesize + @size = 0 @crc = ::Zlib.crc32 @encrypter = encrypter @buffer_stream = ::StringIO.new('') From 85a7bbdf1aa8f5bee3f33d52e9b236b0426bac85 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Thu, 8 Jan 2015 18:30:32 +0900 Subject: [PATCH 08/10] add data descriptor for each entries when encrypto --- lib/zip/crypto/null_encryption.rb | 6 +++- lib/zip/crypto/traditional_encryption.rb | 13 +++++--- lib/zip/deflater.rb | 1 - lib/zip/entry.rb | 6 +++- lib/zip/output_stream.rb | 2 ++ test/crypto/null_encryption_test.rb | 4 +-- test/crypto/traditional_encryption_test.rb | 37 +++++++++++----------- 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/lib/zip/crypto/null_encryption.rb b/lib/zip/crypto/null_encryption.rb index 37e03ede..38dafa8f 100644 --- a/lib/zip/crypto/null_encryption.rb +++ b/lib/zip/crypto/null_encryption.rb @@ -12,7 +12,7 @@ def gp_flags class NullEncrypter < Encrypter include NullEncryption - def header(crc32) + def header(mtime) '' end @@ -20,6 +20,10 @@ def encrypt(data) data end + def data_descriptor(crc32, compressed_size, uncomprssed_size) + '' + end + def reset! end end diff --git a/lib/zip/crypto/traditional_encryption.rb b/lib/zip/crypto/traditional_encryption.rb index 163bf5b6..b57541a9 100644 --- a/lib/zip/crypto/traditional_encryption.rb +++ b/lib/zip/crypto/traditional_encryption.rb @@ -10,7 +10,7 @@ def header_bytesize end def gp_flags - 1 + 0x0001 | 0x0008 end protected @@ -39,12 +39,13 @@ def decrypt_byte class TraditionalEncrypter < Encrypter include TraditionalEncryption - def header(crc32) + def header(mtime) [].tap do |header| - (header_bytesize - 1).times do + (header_bytesize - 2).times do header << Random.rand(0..255) end - header << (crc32 >> 24) + header << (mtime.to_binary_dos_time & 0xff) + header << (mtime.to_binary_dos_time >> 8) end.map{|x| encode x}.pack("C*") end @@ -52,6 +53,10 @@ def encrypt(data) data.unpack("C*").map{|x| encode x}.pack("C*") end + def data_descriptor(crc32, compressed_size, uncomprssed_size) + [0x08074b50, crc32, compressed_size, uncomprssed_size].pack("VVVV") + end + def reset! reset_keys! end diff --git a/lib/zip/deflater.rb b/lib/zip/deflater.rb index f304da77..ffcebb64 100755 --- a/lib/zip/deflater.rb +++ b/lib/zip/deflater.rb @@ -19,7 +19,6 @@ def << (data) end def finish - @output_stream << @encrypter.header(@crc) @output_stream << @encrypter.encrypt(@buffer_stream.string) @output_stream << @encrypter.encrypt(@zlib_deflater.finish) until @zlib_deflater.finished? end diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb index 281bd8fa..05ca23b4 100755 --- a/lib/zip/entry.rb +++ b/lib/zip/entry.rb @@ -143,7 +143,7 @@ def cdir_header_size #:nodoc:all end def next_header_offset #:nodoc:all - local_entry_offset + self.compressed_size + local_entry_offset + self.compressed_size + data_descriptor_size end # Extracts entry to file dest_path (defaults to @name). @@ -648,6 +648,10 @@ def parse_zip64_extra(for_local_header) #:nodoc:all end end + def data_descriptor_size + (@gp_flags & 0x0008) > 0 ? 16 : 0 + end + # create a zip64 extra information field if we need one def prep_zip64_extra(for_local_header) #:nodoc:all return unless ::Zip.write_zip64_support diff --git a/lib/zip/output_stream.rb b/lib/zip/output_stream.rb index f28e5759..e9c54731 100755 --- a/lib/zip/output_stream.rb +++ b/lib/zip/output_stream.rb @@ -123,10 +123,12 @@ def copy_raw_entry(entry) def finalize_current_entry return unless @current_entry + @output_stream << @encrypter.header(@current_entry.mtime) finish @current_entry.compressed_size = @output_stream.tell - @current_entry.local_header_offset - @current_entry.calculate_local_header_size @current_entry.size = @compressor.size @current_entry.crc = @compressor.crc + @output_stream << @encrypter.data_descriptor(@current_entry.crc, @current_entry.compressed_size, @current_entry.size) @current_entry.gp_flags |= @encrypter.gp_flags @current_entry = nil @compressor = ::Zip::NullCompressor.instance diff --git a/test/crypto/null_encryption_test.rb b/test/crypto/null_encryption_test.rb index fa54b025..b9452a03 100644 --- a/test/crypto/null_encryption_test.rb +++ b/test/crypto/null_encryption_test.rb @@ -14,9 +14,7 @@ def test_gp_flags end def test_header - [nil, '', 'a' * 10, 0xffffffff].each do |arg| - assert_empty @encrypter.header(arg) - end + assert_empty @encrypter.header(nil) end def test_encrypt diff --git a/test/crypto/traditional_encryption_test.rb b/test/crypto/traditional_encryption_test.rb index fb1e4ff1..3e743946 100644 --- a/test/crypto/traditional_encryption_test.rb +++ b/test/crypto/traditional_encryption_test.rb @@ -2,6 +2,7 @@ class TraditionalEncrypterTest < MiniTest::Test def setup + @mtime = ::Zip::DOSTime.new(2014, 12, 17, 15, 56, 24) @encrypter = ::Zip::TraditionalEncrypter.new('password') end @@ -10,36 +11,36 @@ def test_header_bytesize end def test_gp_flags - assert_equal 1, @encrypter.gp_flags + assert_equal 9, @encrypter.gp_flags end def test_header @encrypter.reset! - exepected = [239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*") + exepected = [239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*") Random.stub(:rand, 1) do - assert_equal exepected, @encrypter.header(0xffffffff) + assert_equal exepected, @encrypter.header(@mtime) end end def test_encrypt @encrypter.reset! - Random.stub(:rand, 1) { @encrypter.header(0xffffffff) } + Random.stub(:rand, 1) { @encrypter.header(@mtime) } assert_raises(NoMethodError) { @encrypter.encrypt(nil) } assert_raises(NoMethodError) { @encrypter.encrypt(1) } assert_equal '', @encrypter.encrypt('') - assert_equal [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].pack("C*"), @encrypter.encrypt('a' * 10) + assert_equal [100, 218, 7, 114, 226, 82, 62, 93, 224, 62].pack("C*"), @encrypter.encrypt('a' * 10) end def test_reset! @encrypter.reset! - Random.stub(:rand, 1) { @encrypter.header(0xffffffff) } - [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + Random.stub(:rand, 1) { @encrypter.header(@mtime) } + [100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c| assert_equal c, @encrypter.encrypt('a') end - assert_equal 134.chr, @encrypter.encrypt('a') + assert_equal 56.chr, @encrypter.encrypt('a') @encrypter.reset! - Random.stub(:rand, 1) { @encrypter.header(0xffffffff) } - [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + Random.stub(:rand, 1) { @encrypter.header(@mtime) } + [100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c| assert_equal c, @encrypter.encrypt('a') end end @@ -55,24 +56,24 @@ def test_header_bytesize end def test_gp_flags - assert_equal 1, @decrypter.gp_flags + assert_equal 9, @decrypter.gp_flags end def test_decrypt - @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*")) - [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*")) + [100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c| assert_equal 'a', @decrypter.decrypt(c) end end def test_reset! - @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*")) - [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*")) + [100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c| assert_equal 'a', @decrypter.decrypt(c) end - assert_equal 229.chr, @decrypter.decrypt(2.chr) - @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 116, 154].pack("C*")) - [2, 25, 13, 222, 17, 190, 250, 133, 133, 166].map(&:chr).each do |c| + assert_equal 91.chr, @decrypter.decrypt(2.chr) + @decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*")) + [100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c| assert_equal 'a', @decrypter.decrypt(c) end end From f61b3e9b0efc17f81660cb8e10e4e827df1241a7 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Thu, 8 Jan 2015 18:31:17 +0900 Subject: [PATCH 09/10] added test case for traditional encryption zip file generated by archive-zip as reference --- test/data/zipWithEncryption.zip | Bin 0 -> 612 bytes test/encryption_test.rb | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 test/data/zipWithEncryption.zip create mode 100644 test/encryption_test.rb diff --git a/test/data/zipWithEncryption.zip b/test/data/zipWithEncryption.zip new file mode 100644 index 0000000000000000000000000000000000000000..e102b875b651a4cf13aa623003b155f50ede1084 GIT binary patch literal 612 zcmWIWW@Zs#;AG%n;HjVJy60`<@&}9z4En4LKrse}w9K4TL%ouU5)sdT%$$a=SZ1bF zukQI#vN^u@@9d;BmlK5x9$uAyxBK|yNMFmpjsZ-rM_hxz(Tt z_HRm8cs;%5R$jJt@O}-GAi;ot&v&*yiCF!%JH zH%)uhQ}^BomPnV9_@g_AXG6@KNghJZ`G>g_H`$$Rn6%G&`NjsZLuxvu4}X3+zNL1e z^W+W1(~2)vD^2w)sh_Bs|LE|c*-;Anrfn#iWhA!Cr)7$Cwx9DK;i&%;{1?nWch_An z@X7At;H6Hvucl-iDlM4sdB*I;Q>q0geArXTvwZrc?ugQ#a_?}f9=rKle69MT2a{UZP$Kpco9*sq{@4B$@7KldNyr0{Li>< zL;U(U+3xD(C8dG?FV^-Yb@+yB7?<7st@UE%e7%mJ{<=p4-^j3LEt&8{@YVD<^ZYgW z9d1q@55I}}eQwm|7vK?RGTzF--*v>2XOm6!=SN-sA)R}R*H~&v3MN{V_E=qD=M&%0;!y5{SJu$5ktm*&<4c(ZdLM}B}eBa;a;?t}qM8w|j7!my+f X#DXW20B=?{kN_hPngeNZCJ+w*rLGJO literal 0 HcmV?d00001 diff --git a/test/encryption_test.rb b/test/encryption_test.rb new file mode 100644 index 00000000..32b367cd --- /dev/null +++ b/test/encryption_test.rb @@ -0,0 +1,33 @@ +require 'test_helper' + +class EncryptionTest < MiniTest::Test + ENCRYPT_ZIP_TEST_FILE = 'test/data/zipWithEncryption.zip' + INPUT_FILE1 = 'test/data/file1.txt' + + def test_encrypt + test_file = open(ENCRYPT_ZIP_TEST_FILE, 'rb').read + + @rand = [250, 143, 107, 13, 143, 22, 155, 75, 228, 150, 12] + @output = ::Zip::DOSTime.stub(:now, ::Zip::DOSTime.new(2014, 12, 17, 15, 56, 24)) do + Random.stub(:rand, lambda { |range| @rand.shift }) do + Zip::OutputStream.write_buffer(::StringIO.new(''), Zip::TraditionalEncrypter.new('password')) do |zos| + zos.put_next_entry('file1.txt') + zos.write open(INPUT_FILE1).read + end.string + end + end + + @output.unpack("C*").each_with_index do |c, i| + assert_equal test_file[i].ord, c + end + end + + def test_decrypt + Zip::InputStream.open(ENCRYPT_ZIP_TEST_FILE, 0, Zip::TraditionalDecrypter.new('password')) do |zis| + entry = zis.get_next_entry + assert_equal 'file1.txt', entry.name + assert_equal 1327, entry.size + assert_equal open(INPUT_FILE1, 'r').read, zis.read + end + end +end From 5834e999b169026c8e72ae373a6c55f57ee65596 Mon Sep 17 00:00:00 2001 From: Shigeaki Matsumura Date: Sat, 17 Jan 2015 13:05:58 +0900 Subject: [PATCH 10/10] set Zip.default_compression manually to avoid side effect of others --- test/encryption_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/encryption_test.rb b/test/encryption_test.rb index 32b367cd..10026471 100644 --- a/test/encryption_test.rb +++ b/test/encryption_test.rb @@ -4,6 +4,15 @@ class EncryptionTest < MiniTest::Test ENCRYPT_ZIP_TEST_FILE = 'test/data/zipWithEncryption.zip' INPUT_FILE1 = 'test/data/file1.txt' + def setup + @default_compression = Zip.default_compression + Zip.default_compression = ::Zlib::DEFAULT_COMPRESSION + end + + def teardown + Zip.default_compression = @default_compression + end + def test_encrypt test_file = open(ENCRYPT_ZIP_TEST_FILE, 'rb').read pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy