jphastings/rubyzip

View on GitHub
lib/zip/decrypter.rb

Summary

Maintainability
A
0 mins
Test Coverage
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