本稿では、Zip4j を使ってzipファイルを展開/圧縮する方法について、またパスワード付きzipの扱いについて解説します。
- オープンデータ利用のためにzipを展開してデータ取り込みに利用したい
- 定期的にzipファイルを配信するために、ファイルの圧縮を自動化したい
- (おまけ)zipファイルのパスワードを忘れてしまった
Zip4j とは
Zip4jは、Javaでzipファイルの展開や圧縮を行える包括的なライブラリです。Javaの標準ライブラリや他の様々なzip用のライブラリと違い暗号化をサポートしているため、パスワード付きのzipファイルを扱いたい時は数少ない選択肢の1つとなります。複雑な処理はライブラリが行い、実装がよりシンプルになることを目標に開発されています。
セットアップ
Mavenを使用する場合は、pom.xmlに以下の依存性を追加してください。
pom.xml
<!-- https://mvnrepository.com/artifact/net.lingala.zip4j/zip4j -->
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.2.3</version>
</dependency>
特にプロジェクト管理ツールを使わない場合は、jarをダウンロードしてプロジェクトに追加してください。公式でも案内されていますが、ダウンロードはMaven Repositoryから行えます。
サンプルデータ
1つ以上のファイルと1つ以上のディレクトリ、そしてzipファイルのやりとりでありがちな文字化けの確認のため日本語名のファイルを含むzipファイルを用意します。
仙台市の国勢調査のページで公開しているデータが条件に合うため、ここではサンプルデータとして使用しています。
仙台市の昼間人口(平成17年国勢調査 従業地・通学地集計結果その1)(ZIP:839KB)
zipファイルに圧縮する
net.lingala.zip4j.ZipFile
のインスタンスを生成し、addFile()
やaddFolder()
で圧縮したいファイルやディレクトリを追加することで1つのzipファイルにまとめることができます。addFile()はファイル単体を、addFolder()はディレクトリ配下の階層を維持したままファイルを追加できます。
以下のサンプルでは、「248」というディレクトリ以下のファイルとディレクトリを圧縮して「248.zip」というzipファイルを作成しています。
Java
String in = "temp/248";
String out = "temp/248.zip";
//圧縮する対象のファイル
File[] files = new File(in).listFiles();
ZipParameters params = new ZipParameters();
//圧縮アルゴリズムはDEFLATE
params.setCompressionMethod(CompressionMethod.DEFLATE);
//圧縮レベルは高速
params.setCompressionLevel(CompressionLevel.FAST);
//ファイルが存在しなければ新規、存在すれば追加となる
ZipFile zip = new ZipFile(out);
try {
for (File f : files) {
System.out.println(f.getPath());
if (f.isDirectory()) {
//addFolder は配下の階層ごと追加する
zip.addFolder(f, params);
} else {
//addFile はファイル単体を追加する
zip.addFile(f, params);
}
}
} catch (ZipException e) {
//エラー処理(Zipファイルをを削除するなど)
}
ZipFileインスタンスにファイル/フォルダを追加する際には、net.lingala.zip4j.model.ZipParameters
を圧縮時のパラメータとして指定できます。特に指定しない場合は圧縮アルゴリズムは「DEFLATE」、圧縮レベルは「NORMAL」となります。
パラメータを以下に設定すると、無圧縮となります。この場合、圧縮レベルは無視されます。
ZipParameters params = new ZipParameters();
//無圧縮
params.setCompressionMethod(CompressionMethod.STORE);
zipファイルを展開する
ファイルをすべてを展開する
net.lingala.zip4j.ZipFile
のインスタンスを生成し、extractAll()
ですべてのファイルを展開できます。
以下のサンプルでは、「248.zip」というzipファイルから「248」というディレクトリ以下にファイルを展開しています。
Java
ZipFile zip = new ZipFile("temp/248.zip");
zip.extractAll("temp/248");
ただし、Windowsの標準アーカイバで圧縮した場合はファイル名のエンコーディングがShiftJISとなるため、Windowsで作成したzipファイルを展開する場合は以下のようにエンコーディングを指定しないと日本語のファイル名は文字化けします。
ZipFile zip = new ZipFile("temp/248.zip");
zip.setCharset(Charset.forName("windows-31j"));
zip.extractAll("temp/248");
一部のファイルを抽出する
extractFile()
で指定するファイルを抽出することができます。getFileHeaders()
でzipファイル内に含まれるファイルの情報を取得し、ファイル抽出の引数とします。
以下のサンプルではpngファイルのみ抽出しています。
ZipFile zip = new ZipFile("temp/248.zip");
zip.getFileHeaders()
.stream()
.filter(fh -> fh.getFileName().toLowerCase().lastIndexOf(".png") > 0)
.forEach(fh -> {
try {
zip.extractFile(fh, "temp", fh.getFileName());
} catch (ZipException e) {
//エラー処理
}
});
パスワード付きzipを取り扱う
通常の圧縮/展開処理に加え、パラメータを追加することでパスワード付きのzipも簡単に扱うことができます。
圧縮
パラメータ設定時に、setEncryptFiles(true)
を加えてください。ZipFileインスタンスには、setPassword()
でパスワードを設定します。
String in = "temp/248";
String out = "temp/248.zip";
File[] files = new File(in).listFiles();
ZipParameters params = new ZipParameters();
//圧縮アルゴリズムはDEFLATE
params.setCompressionMethod(CompressionMethod.DEFLATE);
//圧縮レベルは通常
params.setCompressionLevel(CompressionLevel.NORMAL);
//暗号化フラグ(falseの場合は暗号化プロパティは無効になる)
params.setEncryptFiles(true);
//暗号方式
params.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD);
try {
ZipFile zip = new ZipFile(out);
//展開時に必要なパスワードを設定する
zip.setPassword("password1".toCharArray());
for (File f : files) {
if (f.isDirectory()) {
zip.addFolder(f, params);
} else {
zip.addFile(f, params);
}
}
} catch (ZipException e) {
//エラー処理(Zipファイルをを削除するなど)
}
上のサンプルではZip標準の暗号アルゴリズムとしていますが、より強固としたい場合はAESを使用することもできます。ただし、Windowsの標準アーカイバでは展開できない場合があります。
//暗号方式
params.setEncryptionMethod(EncryptionMethod.AES);
//暗号強度
params.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
注意点として、zipファイル自体は暗号化によって保護されますが、内部のファイル名については読み取り可能です。ファイル名も隠したいといった場合は2重にzip化するといった工夫もできます。
展開
圧縮時と同様にZipFileインスタンスにsetPassword()
でパスワードを設定すると、展開時にそのパスワードが使用されます。
ZipFile zip = new ZipFile("temp/248.zip");
if(zip.isEncrypted()){
zip.setPassword("password1".toCharArray());
}
zip.extractAll("temp/248");
必要なパスワードが無かったり間違えていた場合は、ZipException
が発生します。
参考「パスワードがわからないzipファイルは展開できるか」
これはzipファイルにかけられたパスワードを破ることを推奨しているものではありません。脆弱なパスワードは簡単に破られる可能性があることを理解していただきたい考えで記述しております。くれぐれも悪用されること無いようお願いいたします。
Zip4jの機能としてパスワード解析は提供していませんが、フリーウェアのおまけ機能のように総当たりでパスワードを解析することは可能です。
まず、テストデータを作成しましょう。
ZipParameters params = new ZipParameters();
//暗号化フラグ(falseの場合は暗号化プロパティは無効になる)
params.setEncryptFiles(true);
//暗号方式
params.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD);
try {
ZipFile zip = new ZipFile("temp/enc.zip");
zip.setPassword("Pass".toCharArray());
File dummy = new File("temp/test.file");
try(OutputStream os = new FileOutputStream(dummy)){
os.write("dummy".getBytes());
}
zip.addFile(dummy, params);
} catch (Exception e) {
//エラー処理(Zipファイルをを削除するなど)
}
続いて、作成したデータに対して総当たりを試行してみましょう。
//この文字の中から組み合わせを生成する
char[] word = "ABCDEFGHIJKLMNOPQRSTUVMXYZabcdefghijklmnopqrstuvwqyz0123456789!?#@$%&_-=".toCharArray();
String in = "temp/enc.zip";
String out = "temp/out";
ZipFile zip = null;
public void analysis() {
zip = new ZipFile(in);
//1〜9桁の組み合わせを順に試行する
for (int digit = 1; digit < 10; digit++) {
if(conbination(digit, "")) break;
}
}
private boolean conbination(int digit, String str) {
if (digit == 1) {
for (int i = 0; i < word.length; i++) {
try {
String passwd = str + word[i];
System.out.println(passwd);
zip.setPassword(passwd.toCharArray());
zip.extractAll(out);
return true;
} catch (Exception e) {
continue;
}
}
} else {
for (int i = 0; i < word.length; i++) {
if(conbination(digit - 1, str + word[i])) return true;
}
}
return false;
}
解析を始めるとわかりますが、PC1台だけでも時間をかければパスワードが解析できてしまいます。パスワード長によりますが、これがクラウドのリソースで並列に処理できるとなればさらに短い時間で破られてしまういます。
大事な書類をパスワード付きzipでやりとりすることはまだ少なからずありますが、パスワードの複雑さについて、さらにはzipでのやりとり自体について考えさせられます。