解決したい課題
大量のzipファイルを対象に、暗号化されているか(解凍時にパスワードが必要となるか)を確認したい局面がありました。一つ一つファイルを解凍していけばわかりますが、その手作業は面倒過ぎて取り得ません。VBAのマクロでZIPファイルを自動解凍することはできるので、解凍できればパスワードなし、(解凍先フォルダーに実ファイルが存在しない等)解凍に失敗すればパスワードがある、と判定する方法もあるでしょう。実際にWeb上で検索すると、VBAからZIPファイルを解凍するやり方として、Power Shellのコマンドを実行する方法や、7-Zip.dllをコールする方法がなど数多くがヒットします。
ただ、このやり方だと、パスワードがかかっていない場合はしっかりと解凍してしまうわけで、スマートじゃない気がするんですよね。何十、何百メガの大きいサイズのzipファイルを対象とした場合、いたずらに時間とCPUを食ってしまいます。そこで、大きなサイズのzipファイルであっても、スピーディにパスワード有無が判定できる処理を考えてみたいと思います。
解決手法の検討
一つは、Shell.Application の NameSpaceでフォルダーオブジェクトを捕まえて、folder.Itemsコレクションで格納されているアイテムを順に見ていき、folder.GetDetailsOfで取得できるパスワードの有無プロパティを確認する方法。対象がフォルダだった場合はfolder.GetDetailsOf(Item, 3)で戻ってくるプロパティ値が空白になるので、再帰で掘るようプログラミングすれば、いい感じで処理を組めそうですが、既にWeb上にYahoo知恵袋にサンプルコードが公開されていました。すばらしい!
これを有難くこのまま使わせていただこうかなと考えたのですが、調査している過程で、ZIPのファイルフォーマット(構造)に心を奪われてしまいました。
さらに詳しく解説されているWebサイトがこちら。
おおっ!ZIPファイルをバイナリで開けば、暗号化の有無が判定できるでのではないでしょうか!
実現への道のり
Local file headerには、次のようにデータが格納されているようです。そして、緑字と赤字部分を参照すればよさそうです。
オフセット | サイズ | 内容 |
---|---|---|
0 | 4 | ローカルファイルヘッダのシグネチャ = 0x504B0304(PK\003\004) |
4 | 2 | 展開に必要なバージョン (最小バージョン) |
6 | 2 | 汎用目的のビットフラグ ※1ビット目が、パスワード保護フラグ |
8 | 2 | 圧縮メソッド |
10 | 2 | ファイルの最終変更時間 |
12 | 2 | ファイルの最終変更日付 |
14 | 4 | CRC-32 |
18 | 4 | 圧縮サイズ |
22 | 4 | 非圧縮サイズ |
26 | 2 | ファイル名の長さ (n) |
28 | 2 | 拡張フィールドの長さ (m) |
30 | n | ファイル名 |
30+n | m | 拡張フィールド |
※汎用目的のビットフラグの1ビット目が、パスワード保護フラグとなっているとのことでした。
これらの情報から、ZIPファイルをバイナリで開き、4バイトのLocal file headerシグネチャを探し出し、その3バイト先にあるgeneral purpose bit flag(汎用目的のビットフラグ)の1ビット目を見れば、当該アイテムがパスワード保護されているか判定できそうです。色々なZIPファイルを作成して試してみたところ、このLocal file header、ZIPに圧縮されているファイルとフォルダーの数だけ作られるみたいでした。そしてフォルダーだった場合は、パスワード保護フラグは0となるようでした。ということは、フォルダーの場合は無視して、ファイルの場合にのみパスワード保護の有無を確認すればよいことになります。フォルダーかどうかの判定は、同じくLocal file headerのcompressed sizeを見て0ならフォルダーと判断できそうです。さらに、幾層もサブフォルダーがあるような複雑なフォルダー構成でフォルダーを対象にZIP化した場合も調べてみましたが、パスワード暗号化した場合は、格納されているどの階層にあるファイルも、パスワード保護フラグはすべて1がセットされていました。つまり、
・ZIPファイルをバイナリで開き、最初の「ファイル」のパスワード保護フラグで判定
で解決できると確信した次第です。
VBAで実用マクロ
ということで、作成してみたマクロコードがこちらです。
次が呼び出し用サンプルで
1 2 3 |
Sub Main() Debug.Print IsZipPass(ThisWorkbook.Path & "\test.zip") End Sub |
次が判定用の関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
'------------------------------------- ' ZIPファイルのパスワード有無を判定 '------------------------------------- Function IsZipPass(Path As String) As String Dim fn, Arr() As Byte, StrSize As String Dim i As Long, i2 As Long fn = FreeFile Open Path For Binary Access Read As #fn fSize = LOF(fn) ReDim Arr(fSize) 'ZIPファイルをバイナリで1バイトずつ読み込み、ファイル情報があれば判定 For i = 0 To fSize - 21 Get #fn, i + 1, Arr(i + 20) 'Local file headerシグネチャを検索 If Arr(i) = &H50 Then If Arr(i + 1) = &H4B Then If Arr(i + 2) = &H3 Then If Arr(i + 3) = &H4 Then StrSize = "" 'アイテムサイズ文字列を生成しファイル判定 For i2 = 18 To 21 StrSize = StrSize & Arr(i + i2) Next i2 If StrSize <> "0000" Then '暗号化ビットでパスワードの有無を判定 If Arr(i + 6) Mod 2 = 0 Then IsZipPass = "Passなし" Else IsZipPass = "Pass有" End If Close #fn Exit Function End If End If End If End If End If Next IsZipPass = "実ファイルなし" Close #fn End Function |
フルパスで指定されたZIPファイルにパスワードが設定されていた場合、”Pass有”を、パスワードがない場合は”Passなし”を返す関数です。フォルダーやサイズが0のファイルが圧縮されていた場合は、”実ファイルなし”を返します。複数ファイルを判定したい場合は、このFunctionプロシージャを連続で呼び出せばよいでしょう。
コードの解説
ここから先は、処理内容に興味ある方だけ読んでいただければと思います。処理としては、まずZIPファイルをバイナリーで格納できるバイト型の配列変数を確保します。念のため、ファイルサイズ分確保していますが、お目当てのLocal file headerは早々に現れるため、この配列がすべて埋まることはありません。
1 2 3 4 5 6 |
Dim fn, Arr() As Byte, StrSize As String Dim i As Long, i2 As Long fn = FreeFile Open Path For Binary Access Read As #fn fSize = LOF(fn) ReDim Arr(fSize) |
次に、ZIPファイルを1バイトずつバイナリで読み込み、配列に格納していきます。ZIPに格納されているアイテム(フォルダーやファイル)の冒頭に存在するLocal file headerのシグネチャサイズは4バイト = 0x504B0304(PK\003\004)なので、それをループで順に探します。andで4バイト一気に探したり、4バイト連結させて探すより、先頭のバイトから順に探して合致しない場合ループから外れる処理のほうが速度が速くなる(気がする)ので、IF文が深くなってしまいますが、こうしています。
1 2 3 4 5 6 7 8 |
'ZIPファイルをバイナリで1バイトずつ読み込み、ファイル情報があれば判定 For i = 0 To fSize - 21 Get #fn, i + 1, Arr(i + 20) 'Local file headerシグネチャを検索 If Arr(i) = &H50 Then If Arr(i + 1) = &H4B Then If Arr(i + 2) = &H3 Then If Arr(i + 3) = &H4 Then |
そして、お目当てのシグネチャが見つかった場合、そのアイテムがフォルダーかファイルかを判定します。判定は、ファイルサイズが格納されているバイトを参照します。
1 2 3 4 5 |
StrSize = "" 'アイテムサイズ文字列を生成しファイル判定 For i2 = 18 To 21 StrSize = StrSize & Arr(i + i2) Next i2 |
ファイルサイズが格納されている4バイトがすべて0だった場合、文字列変数StrSizeには”0000”が格納されることになり、フォルダーかファイルか判定できます。フォルダーの場合は一律パスワード保護フラグは立たない仕様のようなので無視し、ファイルの場合のみ判定に入ります。パスワード保護のフラグが格納されているバイトの1ビット目が0であればパスなし、そうでなければ(1であれば)パス有と判定します。1ビット目の判定には単純に1バイトを2で割った余りを使用しています。判定が行われた場合、Functionプロシージャを抜けます。
1 2 3 4 5 6 7 8 9 10 |
If StrSize <> "0000" Then '暗号化ビットでパスワードの有無を判定 If Arr(i + 6) Mod 2 = 0 Then IsZipPass = "Passなし" Else IsZipPass = "Pass有" End If Close #fn Exit Function End If |
もし、今までのループで、Functionプロシージャを抜けていない場合は、実体ファイルがない(フォルダーのみ、あるいは、ファイルサイズ0のファイル)状態であるため、”実ファイルなし”を返します。
1 2 3 4 |
IsZipPath = "実ファイルなし" Close #fn End Function |
この手法であれば、Zipファイルに、大量かつ巨大なファイルやフォルダーが複雑なフォルダー構成で圧縮されていようが、一瞬でパスワードの有無が判定できるのではないかと思っていますが、いかんせん、実務での使用が不足しているので、動作結果などコメントいただけると嬉しいです。
コメント