知人からその知人の生年月日がパスワードのパスワード付きzipファイルを渡されたけど、その人の生年月日なんて知らない場合の対処法


すぐに思いつく案としては、２つあります。

1. 知人に生年月日を聞く
2. 頑張ってその人の産まれていそうな日を入力する

案１は、知人は「私の生年月日がパスワードです。」って言って渡してきているので、当然自分が生年月日を知っていると思っているはずなので、その人に対して「すいません、生年月日を存じ上げません。」などと言うのは日本の文化としてはなかなか難しいことに感じます。
そういう場合は止むを得ず案２でしょうか。

## パスワード付きzipファイルの展開方法（Ruby）
decryptできるgemを探したら、[ZipRuby](https://github.com/callumj/ZipRuby)というのがあり、使いやすそうだったのでこれを使おうとしたのですが、展開を試みる時に一時ファイルを作ってしまうようなので、[rubyzip](http://www.rubydoc.info/github/rubyzip/rubyzip/)を使うことにします。
名前が似ているのでどっちがどっちなのかわかりにくいですね、、、

使い方はこういう感じらしいです。

> ```ruby
> Zip::InputStream.open("my.zip") do |io|
>   while (entry = io.get_next_entry)
>     puts "Contents of #{entry.name}: '#{io.read}'"
>   end
> end
> ```

上はZip::InputStreamのopenにファイル名のみを渡していますが、第３引数にデクリプタが指定できるようです。

> .open(filename_or_io, offset = 0, decrypter = nil) ⇒ Object

Decrypterを調べると、`Zip::TraditionalDecrypter`というのがあるようなので、これを使えば良さそうです。


## 生年月日のパスワード（文字列）を生成する方法
日付は普通にTimeとかDateを使えば良いのですが、現在わかっているのが知人の推定年齢だけなので、年齢を曖昧に指定できる必要があります。
というわけで、こんな感じにします。

```Ruby
# 推定年齢の範囲
RangeOfAge = (25..30)

# 年齢の範囲から誕生日の範囲に変換する。
def to_birthday_range(r)
  (Time.new(Time.new.year - r.last, 1, 1).to_date .. Time.new(Time.new.year - r.first, 12, 31).to_date)
end

# 日付からパスフレーズを生成する。
def to_passphrase(d)
  d.strftime("%Y%m%d")
end

# 使用例
to_birthday_range(RangeOfAge).each { |d| p to_passphrase(d) }
```

## 並列処理
並列で処理したほうが速いかもしれないので、簡単に分散処理してみます。
年単位でブロックにして、いくつかのスレッドでブロックを処理させるようにします。
また、根本的な話で、正解にたどり着けばそこで目的達成なので、馬鹿正直に全部試す必要はありません。推定年齢の範囲を指定するときに、おそらく正解であろう年齢を範囲の真ん中あたりに持ってきそうなので、真ん中から解析するように並び替えておくと効率がいいかもしれません。

## 実装
```ruby
require 'time'
require 'thread'
require 'zip'

# 対象のファイル
FileName = ARGV[0]

# 推定年齢の範囲
RangeOfAge = (ARGV[1].to_i..ARGV[2].to_i)

# 年齢の範囲から誕生日の範囲に変換する。
def to_birthday_range(r)
  (Time.new(Time.new.year - r.last, 1, 1).to_date .. Time.new(Time.new.year - r.first, 12, 31).to_date)
end

# 日付からパスフレーズを生成する。
def to_passphrase(d)
  d.strftime("%Y%m%d")
end


# セクションクラス
class Sections
  def initialize(f, r)
    # 範囲の真ん中から解析するように並び替える。
    a = r.to_a
    b = []
    until a.empty?
      b << a.slice!(a.length / 2)
    end
    @sections = b.map { |i| { file: f, range: to_birthday_range(i..i), assign: false } }
    @semaphore = Mutex.new
    @result = nil
  end

  # 未処理のブロックを取得する。なければnilが返される。
  def next
    @semaphore.synchronize {
      return nil unless @result.nil?

      s = @sections.find { |i| !i[:assign] }
      return nil if s.nil?
      s[:assign] = true
      puts "assign #{s[:range]}"
      s
    }
  end

  # 完了を通知する。
  def finish(s, result)
    puts "#{s[:range]}: #{result}"
    unless result.nil?
      @result = result
    end
  end

  # 結果を取得する。
  def result
    @result
  end
end

# 解析クラス
class PasswordAnalyzer
  def initialize(s)
    @sections = s
    @thread = nil
  end

  def start
    @thread = Thread.new {
      # ブロックを取得する。
      while s = @sections.next
        result = nil
        s[:range].each { |d|
          # 完了していたら停止する。
          break unless @sections.result.nil?

          # 復号を試みる。
          pass = to_passphrase(d)
          dec = Zip::TraditionalDecrypter.new(pass)
          begin
            Zip::InputStream.open(s[:file], 0, dec) { |z|
              first_entry = z.get_next_entry
              next if first_entry.nil?
              r = z.read
              if r.size == first_entry.size
                result = pass
                break
              end
            }
          rescue => e
          end
        }
        
        # ブロックの処理の完了を通知する。
        @sections.finish(s, result)
      end
    }
  end

  # 処理の完了を待つ。
  def join
    unless @thread.nil?
      @thread.join
      @thread = nil
    end
  end
end

# 実行
sections = Sections.new(FileName, RangeOfAge)

analyzers = []
4.times { analyzers << PasswordAnalyzer.new(sections).start }
analyzers.each { |d| d.join }

puts "result: #{sections.result}"
```

## 結果
実際にもらったファイルじゃないけど一応結果を載せておいた方がいいと思ったので。

```sh
$ time bundle exec ruby unzip.rb sample.rb.zip 25 30
assign 1987-01-01..1987-12-31
assign 1988-01-01..1988-12-31
assign 1986-01-01..1986-12-31
assign 1989-01-01..1989-12-31
finish: 1987-01-01..1987-12-31:
assign 1985-01-01..1985-12-31
finish: 1989-01-01..1989-12-31:
assign 1990-01-01..1990-12-31
finish: 1986-01-01..1986-12-31: 19860601
finish: 1990-01-01..1990-12-31:
finish: 1988-01-01..1988-12-31:
finish: 1985-01-01..1985-12-31:
result: 19860601

real	0m0.898s
user	0m0.805s
sys	0m0.131s
```

はやっ、、、

## まとめ
- 生年月日や携帯電話の番号などをパスワードにしてはいけない。
- パスワードは共通の認識でなくてはならない。

## 参考
- [rubyzip](http://www.rubydoc.info/github/rubyzip/rubyzip/)
- [ZipRuby](https://github.com/callumj/ZipRuby)